LLQC2M2IMEJBJQXZTKC3OAKG5WKHSERXKAKCYHQRUZZD6CVRIHAQC JCUTYA6OSE5ZCJLXFWDFLLNJYV757UE7ISO33JLB7ILV5QHWUDGAC 6WDBV52ZFEYAUK6L66LDOKJ5JGHP63VY5R4NDOROZDY6HZJF45KAC JLU2RMC4WICQSGLPVSQNJ4OSAUNHH3IP3FT37WIPBCKCUUQE65GAC 3OC7AIC7B6XFGODWNAOSQ25YQ27RUEBNHUVZG6U2PK3CXV7Y7GHAC VSA3FN7XPU364N34UWUM4UIB4PSYLU3UZB6VMXAAEQY7PJBRWHQAC 62JEPVQ34SOTQI6VQNLGLKS5O4KFU52UKAVDHN6N7G5T6Z5EZO5QC 6ECYOEHY3BHYC6VYMR2AJV4H54NVKSTUKOMFBI3HRDS5V2JZ42JAC BSNH2SEADMJXSPGZVJIUZ43F72ZWFHZFHSPGESNKCQDSH2TAS3LQC UX2N6BSKWAU7RETG6DRMHFWHRC3UAJOGQZSBDUZER6WKNAVPHVPAC AYS3Z3TXOXF5ZJDLSBWEUIOWXJEZXLXEBZS5ZRJC2IRDVM7KTSTAC HNZMFBMQUT56TBTOYQYSOP3L5ZD6MNYRGEPHKLW6MDEHVBDFHYNQC TYM5V6UKH2LDNVESWE7HSSMVTOY5ITRUTPFAKRYLMVSEHGWSP54AC 3RCPFBL5CYIQZH7M24AJ7AH7SRADDJUWGRCZ3TSTGEKY7C6CWRUQC MZ77IFRCNPE4T7HFE4ZN3S6ODBIRITPZ47UOXRAVBB27S5IJXYOAC XNVOMQKM6KBP26VNJQOWW3ZQC4VYXQFWMBKYUX77YXC5PTWDPYCAC HPQ3UXZCCQM4KFVQZH4BW42MFVCUJLXPM7VGMOLDCPB657WED6XAC EPDWNONEMQKCDZOVNEEHUKKQMCYIFLNPOPUBFYQFSV2U3D3CTV3QC 5OJ3HXK4XFYLAK5FCAJH26ZDMK34N4EAE6CBHMGAG5MODIKLZ5HAC UKTYM2SZUHENDMKKWQGFBZ76PZLXBVQ7EVMGRTQRPWVKKHXJU2XAC COMBEL3F5MWESKCIKONPJE6VKEKXHMC3J2777EBJLQ47WE2O2XIQC QIZAPGRBZA4UYNP4KDMXNTL4XM55N5WGLGZGTEOF5VMJ6TXFZFIAC AWJQSK7UUIGFWHBSAONXWXPAX7C5LGCOFNMDXQS2JVNPB26EJCXQC MPISJX7LRSCBP3YV5HLN76XFEZ3JDXKTCSCM7SBF5Y7SFNQUFKIAC GGGV42XCLQ3JCGT4JWYMO4F6HDBURUM7C5TDAFNEVYFBTUG3OG5AC 2VK5EL2HIOV7F5WVAOP7DMK2KZLD55FW2KFJY5LIKPN4LUULA73AC FULK37LWMRZQBK7EZFO2YVE2CNP26QXVKF652JM7UC77I3LBCRKQC WGYP72EIFXJ3AF6CHT3JYHLSNSKX637P2CCYWDRYQRULYWI6WE5QC 44BTGR7UG3PYPZQ5EHUOOSMJ4P5NV2FO23EFHMKVOQLFRTT6TJTQC XJCP5AVR7VWO66YI4TOKQD5LEH72YCTMMPCMO5PEG42C5LXXM6IQC FO6ZBUW6N7MTC5EXKJ5MP5NAUEBDM24DD5CVOJ6QYSANNKL2HNEQC 43PXJ6V7C6XPJE6LCOV7QI4UTR2M7PW7QA2SPC5L4QHPPI7GOVBAC UKQJBQAGAHQHL2H5Y6DMGV7QQ3EVCBIDBFGA4WATEWJDYQPZ26VAC MCNS5LRJAOKO6YAE34RIACBVPQNRFE4GYTYDELJFJ5GSVPONO36AC CLS4IP7PYEJMWQZFQMYER6VEIDHR7XIE3ZSCGHU6TPXAGAJP3T2AC JOXYEEOEUDQQQZCH43K3YG74JAKY65FBH47KGY7VKAGXJ4TRHJ5AC STDBUWM2NLJBMNKWFYISYTOKGU7VXUBKNHRN63TX5OINWNMGNYHQC FX6XTKP77BYLOGIJ7ZB7V5NGMMY7LRRG7X2C2426ANTF5NJNACIAC DZMHZBYSL73BWS7OX3BGOYCITVIGFKNLUXNQUAGKXMGXDE4ZRL6QC ECEFD4ZKDUUPLXBOPIK32SRGJY5EZDC6YKYNOG5YUH24OJ2PKKZAC PLDDNZJCDQ2CLVYJBO43NRXJS2ARW6C7XBXJJLRW2USCBHKJ4FQQC DGCSQ3UZPKUYOPKGLTOLCRBXHJC2NQT6P47WSSHAKDU6OWRBKMHAC TSK2OXU2FTB2X44SN73Z4W4O6IDV6G6V3QU5UVTAGQY52Z7AHECAC HKV72RZVJEOF5GCHCRKEBGC3FQN7AYETY7LKEJUXVIQAB4QPEPYQC 3PSFWAILGRA4OYXWS2DX7VF332AIBPYBXHEA4GIQY2XEJVD65UMAC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC 3VHUIIATPOF7FXB7NTL5MESCV5BCQACII2D7QZ4UIUCBX3CWXMMAC 73OCE2MCBJJZZMN2KYPJTBOUCKBZAOQ2QIAMTGCNOOJ2AJAXFT2AC MTJEVRJR5GLWUSK7HMIM4UXM6GS6O6YCRWJT3DUSU2RYMHCQNOEQC LF7BWEG4DKQI7NMXMZC4LC2BE5PB42HK5PD6OYBNIDMAZBJASOKQC VJ77YABHVJZWJKLHAGIPC562GYM73AUGRLCP4JLKP5JPWPT2RIHAC SQLVYKVJ5O4UMKTT56LMFPDQX66SZJJ7FZSFEN5MTWPXXWL7X3WQC NQKFQSZEFIQTIJXEJ64KX46JXLWUUFXVRTQCPM7HF4DUHT2QHZAAC MD3W5IRAC6UQALQE4LJC52VQNDO3I3HXF3XE2XHDABXBYJBUVAXQC MUJTM6REGQAK3LZTIFWGJRXE2UPCM4HSLXQYSF5ITLXLS6JCVPMQC ILOA5BYFTQKBSHLFMMZUVPQ2JXBFJD62ERQFBTDK2WSRXUN525VQC 3GFQP6IRHABYMDAEXEMM2HQNEUY4LT2P72PI3KXV4M6PSQT3SFLAC LXTTOB33N2HCUZFIUDRQGGBVHK2HODRG4NBLH6RXRQZDCHF27BSAC KOTI3MFGQ4PDS4I75JIJG734LTET6745VGTSMNFYYASVIO6H2KPAC DSLD74DK3P6J2VAFCYF5BGTHZ637QTW3PDHOUHFACDZU66YNM3IAC UHB4GARJI5AB5UCDCZRFSCJNXGJSLU5DYGUGX5ITYEXI7Q43Z4CAC AJP4OSTJSREBMJ5FOAHMOF6D4LKMKMRHU5NUURDLVCB4ADPX66TAC NUZFHX6IUV2KXZOIJQTD5VIU7ELDQCFPDXYBUNQGWLKH3OMYND5QC APYPFFS3G6TDEUMIHQGMDBJNRNDTCNTPKI5M2AFACJ73P725XQRQC 2LC3BM2NCIR76UILI5D4DVC5KYJSBVHDNMOC5G3TOJNCRLX6PZEQC V3EABA35RWCOOU5OMIYRWXAKZOLHO4XPGTPOKY24RR2LOAD7ZQAQC CNCYMM6ABOXCRI2IP5A4T2OGBO5FQ7GWBXBP2OQYL4YET5BLJCGQC LNUHQOGHIOFGJXNGA3DZLYEASLYYDGLN2I3EDZY5ANASQAHCG3YQC Y2ZIPXEMMCY5GHJDDF7OMRKEQYMSDR5QTJDA7Y2SBOTHAJKHWVOAC GL4Q5WCVMOBEKW7SMBKRSL3DRG2NSTXRI7VQFK77OXAWLBDKWTNQC EKKFWP4D2MNOHU265UCJU37KIFQV424CRLVASQMHDYUYY5T67D3QC VSBSWTE4IVQDRXLPQ7VTDIIEBEF7GMGRBHZ2IA73ZR6B2KZWI5JAC DGK5BPVI6PAD3WK2ZB2ITMBE6WYSU3ZR7TV7RTCQ2WJQ4RGJE5RQC VG75U7IM2ZQTGM2QETDT6QQ4CSLQPB4APK436POAAQJWOMINPIJAC 4J2L6JMR7NZBGCNX63CL2E3AIB7P7QTCC7QQBPNAEPQ7ISQXL7EQC 4VKEE43Z7MUPNIAOCK36INVBNHRTSWRRN37TIKRPXPH3DRKGHHAQC QKAMUWSB6GWKEGLXFKALGCIU7HBTZ4YGLIR7TLA6ZZCUK7WNCNUQC 3TCZ7ADHZ4YALUYII4QRSITV2VUKN645P7D7XTXD7ASFZTAP7THAC 356GY7IQ467QQMIPFMEETHTXLSZE65HA36PXSOW4KKXBUHSMBQTAC ZLJGZYQGQ2S4UFWTVF4PQDSGMP6A4IS4GDHCMBAAA5SK2N2NWR3QC WAR3HXHTN7JZVV6TFMU2F3QYAG6NDH7DN7KKPTM2ICEHRNQYP6PAC RMKMPFT5L67WIFWIO4GTC6XESX6UPKNL4GPNQLOBC5CXSUZABEHQC T3B4NLV33PBD2L3YL3MHSOXZUWHDOGHPWLKKKHEBKJFSHYQWUK3AC CVSRHMJ2BM4LPVG67ULIVQMP2NW3YY2JC2ZQBEA6EB5KVM4O2L5AC Y4SPXCM3PKARGUU22FNBEDRU7S6CJSNYVAA76JYH4I4EMMMKP6LQC NZKYPBSKYJ7NQU7ABRHLYZ2P2P5V2UF76OLRURGTGRUB54R4SPBQC HTWAM4NZFOY463TNSKYIM2EWB7QNBGDRRTTGHF5N3Z4TGC7Q3SFAC 6PCE7VUWEE5UVSE3ZZSQAMGUJDDHOBNO2MVD6POUWFP7UY7WOZHAC DRFE3B3ZKRG4RY2R5Q3SDFD3LH4EXUX3CZCDFBNAXVI2SLDS57PAC JFFUF5ALUWPDM7IEDEZVAYG2SVXO334STONRGKVB3QKY2TT5QGBQC LAW2O3NWVFTPBSKIMIXPAGYBDOCHYJNKCAVWKNKH62G42DIKZCYQC FZBXBUFFNRE5ZJO5DLRU375HOXT2B7FO35XD7BTHHUXSARVWDFLQC EETIR4GXZBA5DEXUQKI6XMC22SFHZIYTZRR4U6BW6BT3RZA32FHQC MSOQI3A5BC5PY2MZXZQAQ4EQDT4KICQJPN3YUZVDYTWXSPZWBLIAC LLAOOMULEBXFMIGRBY6LRVEK4RXQGPNTFVWMCZNUEJZHWC7UGUEAC AMOPICKVRHMQERJLFPMAAEBV7TL5QACGGSBJWRCMV5R5O3KDVETAC S2YQBEYCOBS4ADO5VX4YLAWY6CJEQOOZM3THYTDOTXM7ADID6PGQC M6TH7VSZQGKDB7SFNN5K52WWAX5VTVNT6GOKNKTXPVZBT6NEYDOQC 2JLVAYHBQGIYFYLPYP5MC7V3DGTSUKLKTFSAIDG4XZFWVDU33SNQC IFTYOERMW7P3I24WISZN35X3GWJ5MSMRYDRBK3L52GCZTPP3CWZQC CUIV2LE5D6GUQ4NU7K2TGUVO5CTUXVJDRCZUIV47LXTOUSEPEJHQC GJLOKCYKETWXJXBOS5222HVZIKBDOGLLR5QLUZYCTZG7FBYDTQMQC MYC7XR5QOT2AXHF6UNGSNFFD5VL6UHGUZQBP7PWWLZ5NNXE7UMTAC IWYLK45KJSPRXKW55OD4GEPMLTYMMTXNFJJU26JTZN3RE35DWSCQC FYS7TCDWKNRNOJSGRD2JMU4B2LHX5S63ZISM7YF7KZYEYLVCIKIAC 7EQLPB3O4DPUWGILY4P5D32SSIKL63QWWU5XRL2HISGNJXFWD2SAC EMHRPJ3RAVIVJEQIRXIVDGENV6QHUUGXXRWTJ3BXC7SZNC66VK5QC 7NQCCB34KI7PFWPR6EWLBTHLPHMZK25PVZKHK7HEOZKTKENACQHAC H3ECRBXFBASVUPMZYM5APUK6AR3UF2O6I7BF7KQPV3YHBNT6YZWQC AQQQNDTL52Q2VO3XLEGYKHTU2YSRAB4ACEGVPSWYD2Z6WA6Z2YPAC 5BMR5HRT7GN5L4XB4ISP4JJP3ONZESHEEQBCTQE4EVEDL7MBSDGAC BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC D4DETYG5FMOYM2U5JIDZ3ADZ2YDSAU6XP234523DUMDQWDCLJXTQC GSIYZ7K25HG2BYKUF3G5JKI4M3MBWC2LRC5KJIHKG2JES6GYX5DAC EVZAO6TCTTJCBZNRFW6YTIYVCR6S5OQ24S7TS4WJYWFBU43N24XQC AIP57TM4LFTWNVGTPJQXKAIY3H5CJWNG5LMK6ZH7LW7CICVP5O2AC Q2VIF6IBHUAMETHJGJKNQNVY4TTBRKRFQW7RWRMM3IGC4W3TEP6AC XPXYD64Y76UT24GENTC6IWYNYWRCTZZE55YCBGWJT7YAYHIFZLXQC K5UKK5UVQBJN5Q27LLMJ7YQKENNHZOARPJQCJQOXQ4KFMKLINELQC XUGDTYW2OALZNGX52BJXFYW2IJ6YSXA62ANG2NX2KDWULYAPZYOAC H2DPLWMVRFYTO2CQTG54FMT2LF3B6UKLXH32CUA22DNQJVP5XBNQC QDSPFWFFL2JEGKBPPXQOGCWMB3KELKRMZNBSYNYUZIRNT7RL43JAC TXHMMX25XTR5BQLKHQXIT5TZBFW2KZ54XY3CCL36ZJYJWPKGKC6QC ZDAJXYIXA3NVIUJIVLKWQB2TBNYGSFCPR7I6LVNK2OIJJ3O5ZSUQC OYXDYPGSJK2QICJ6RBA7357WT4FSNAWRUT77YLQHT3F3VYMWGNFQC IO7JTVO75DECUIYIK5VY5NGYVSAVBBVYNRB2YEY2YX3V5VDEUOQQC N34S6GWNCQQNEOXZRHPWSRZE7BXFPRIJHUYP32OIPQ4436VS25FAC HTZ3WRQHWJHMRR354RCPYK3L7OB5KRDJPJEL3Q4Y7IVNCYN2TSJQC TVZWTOE3T54QML767ZPFXBAFACGQ7CI4NIU63YZP5STFCWSAJ7CAC JYVO76NCD5B2R3JL5MDOI4OQZFPVAU6FGMMINWYR7J4BZKHR33GAC IMZFR5362472CHLZWY2EQAZ7PEDYLYOYE5YDIHFTTDDTQOQWPM2QC HU2TOQI2ZSP76BREOPYQBKYKJ6MOZKBZ5BOKSSKGXUDLZSQ3JBQQC LWPRYDLW45QYN5DZGH75P3RDYPZKABYQVDM2ZJYPTTDMTXCRNKUQC HALS7E5UGKCP3DFY456F7Z3Y6WNGIABOCV2SHT34D5ZAGNCPV5PQC X3F7ECSLGXCH6NBSDIH7LY47I4EG2RR5VFPEMM6ZVDYQIGFID4HQC LERERVPHE5SEWDHQ7IAGQSXUAI2QHQJ33NBNRMRXZ34X7P23I2IAC ESETRNLB3MIJ2SID6HJMMP52FEVUBLGK2HLWD75KDQZAKQMKSF2QC CVGE3SIGJRGCLY3A2RBPGFXAEKVZXUUIZQLRHJLM4VPUM4SHEZIAC YTSPVDZHEN5LLNMGIBUBLPWFWSFM3SOHBRGWYSDEVFKRTH24ARRQC A2NV3WVOKBOWBCSV3K4I6MO5LSVSSUZVNH226HV2HDCOMSPRVSSAC HIKLULFQG7Q7L4C5KXR3DV3TBZ2RGWXBJJXIGSE5YQWF37AJOYZAC DHI6IJCNSTHGED67T6H5X6Y636C7PIDGIJD32HBEKLT5WIMRS5MAC S2MISTTMPEULTO6WRO4Q4NRUO7XC2PTZW3UBR7K7SO6JPZO6HBHAC Z4XRNDTRTGSZHNB65WNHOVUBFW4QWQABLVSK4RM3QJHGK33DMRJAC FHSZYAZ2KCHJM4BN2TAPYZMWTLTIE23SWKDYLCQOQIVL4263HDRQC HOSPP2ANSW654DYRTC6CQUQA2GUKV6T2FI7QBKXD2DZS3R32IMGAC RTDYYP4HQI4RLAISRXGB6TFWALBXSO3EQ4JCABRZM2TOQEJOGB6QC ULKLJBN6Q2EXYOXGIJLJ5NZPZD2MQSWR63Z2I3KDYJDAJQA5VNZAC 5DOC2CBMBDMAOJ7IKLDGVRCY4SNPCJTTF7DK7WGNLPGNV4AWVJNAC XVR2O5PIN4KDGEIFAXR2A54Q2GDYJHXUIHRFI74UU736M4R4CLVQC 3TFEAQSWVFGSH3ISZ4Q3DFR3YPPWHEIBUEVR3XWB7QX6VKHW455QC 2RXZ3PGOTTZ6M4R372JXIKPLBQKPVBMAXNPIEO2HZDN4EMYW4GNAC B3IWYWSRDSZ7AG5HDS3TELNTG2IKRZYPI25B6LJGVFAJYTHVXZZAC XNFTJHC4QSHNSIWNN7K6QZEZ37GTQYKHS4EPNSVPQCUSWREROGIQC KWOJ6XHEE7ERLFJ6FBXCL73DE6OFJQ7LXNXAN44G5P5EXFDH5HIAC OIB2QPRCB4MAVZV5NCEKSAL45ITT6V4BYSET3Q2VCT3WBOIC4QVQC OYVFFWBK5IL7IPAF5HGFONJ2NEBRR3GTISPFROG7HJDEZYJAM7VQC J2SVGR2EQEROXDDMYZOCELD2VDYQALGZYRSZ4WGMTACAGMRPJ7UAC 4KC7I3E2DIKLIP7LQRKB5WFA2Z5XZXAU46RFHNFQU5BVEJPDX6UQC RYGAFBYIL3P2Q6SXBOXHNO7LYLMBKPUPAYLHSPYJW2ASGGO3WAMQC HGC5RGJPK34K5HRPG6FB7VIKOT6VTWVEC3CBYS5QVH5LVKAL6WGQC TAHUAI4QDU4PFNR5O5ALFM4BVOW6COA6MW7CHSGQXQQXDVQW7HBAC PFT5Y2ZYGQA6XXOZ5HH75WVUGA4B3KTDRHSFOZRAUKTPSFOPMNRAC NDHQN23GI2IFUYGNYSO4BC467L37CQDDYL4C7NYCLD47QHOG6WFQC UPCIYZEUIFO2UJ3WPAFOD7VLNZEIIYYGJQGEMJOP5TSSE5PM4ZWAC R5OKMVVCPAKL2IUMIY7A7ZMTJQZS6UWKW4EVLAVCPLPVNI5DCEYQC 3MAZEQK5AR3IJJ2ENHHYDPDICIK645NE5QWR54Z52BHGHE6VR5XQC BYG5CEMVXANDTBI2ORNVMEY6K3EBRIHZHS4QBK27VONJC5537COQC 5UG5PQ6KN7EUQTYEI5GYNSW66BVOA2M4CCRYQJY5IKTQNNSSZPAQC IRCKL6VNSFB7TQEKPQUPJCN37N5QW7D54DSZMESVXGK7NEHGSIPAC 2ZYV7D3W2HPQW2HYB7XDPM4T7KEWPUFPZ77BDLCCDSCLRPJFK6PQC Z5HLXU4PJWWJJDBCK52NBD6PIRIA3TAN2BKZB5HBYFGIDBX4F5HAC IMSC7J5ERUFHEYCQZ4BMKWFDWCKZ6GNSRZGCQXHVM4W4HBYGTLGQC CG3264MMJTTSCJWUA2EMTBOPTDB2NZIJ7XICKHWUTZ4UWLFP7POAC 2POFQQLW42ZQCF7NBTIFLYKXBYT5PVSC3T5UOURIEPYNFVBN2MKAC 537TQ2QNPKPG322I4OIMN5IY22S45Z42LEBBZ2IN5MVM355BEJTAC JY4VK7L2JKRWRV45QEMGLWPFAQRUWKFHMAL6DWNYEDCKO5Y4W5FQC SPSW74Y5OJ54Y7VQ3SJFCJR5CYDKTR4A3TOEVZODDZLUSDDU2GZAC QCQTMUZ7M3BKJFTKXTTXL4TS4CAQNIUNK3LR3WQIJDU3VVTOPS6AC 3TDOZESEOYHGF6LYKR6PYSPNFI3QUGED2BKM5LUDEKJKRIX3ACEAC QCPXQ2E3USF3Z6R6WJ2JKHTRMPKA6QWXFKKRMLXA3MXABJEL543AC PTDO2SOTXEI6FROZ2AVRFXSKKNKCRMPPTQSI5LWD45UVGDJPMSGQC PHFWIFYKFOGVX7CEAMGJ3FDY6LL5QSZ7T7CTCZ66WMNXV6C242FAC DAENUOGV7KR6MZVXS36HEN3SZC4RFIS6REGAFVBOFEPO76EUDGIAC HMODUNJEQLZ3W46GKYIDL55F6COVXHTIC6UW4AK3SXOOKOPE6NNAC JJDT2X4FKYC5I3CPTV4PNOALPFXQNG2SSLZJ36QFZIBUXY2ZAXXAC OGUV4HSA7XGSQLUVWBAE3AE263Z7Z6G3BZOB4CN2AOYD2DEJMOZAC TGHAJBESCIEGWUE2D3FGLNOIAYT4D2IRGZKRXRMTUFW7QZETC7OAC X75QPYVWFSE7RVAJXRPA2I3AJOXOP653W7Y7NZG5XAEBR7MZU5QQC AOIRVVJARCGTWTRE5MAAU4YQAGD5J4HTR7XCS63UFAUY3A43L6NQC IM3RBHY2QI5UHLPHT4QB3YECS7CRBEFE765DDKXOW267AOQZL5QQC ZHLO7K3MQNI6OMK6226SSO2Z6Z4ZXF4T73VOG36DVAG6CHR6OHWAC ZPUQSPQPQFVRUIHGLAWW3IDBYODIWDHO62HAC3WWF5TM3CIJGHNQC EBBFOW4X72TN445NM5MJQPEKN3PIQMRCM3V56YEEFY6PBGLPJO3AC 4KOI3E6RUU7IZ3WFPHUVZUO2MANLMND3T7ZQENAV5ESCU7HDLYTAC DLQMM2656JHXX3ONOEM6UIOXKFJFT5QT7RHWK7YS2W77PVZWHRSAC AMSESRTH4T7EIEMXEFPMZFC55QAOVSWAN2XOQUUEB5ECHRDZUAYQC KOYAJWE4NJ2J4X3SHEAVMRXYZPZGOMTI7OX3PTUQIDIZ2GQI6UKAC GN3IF4WF352YK5K4YHVMAIMPL7PNTCEMDWW22PTKDOXKV2FZJ7NQC AVLAYODPMKCDBUFJSTGNUXIK74V3NDCBH55DBBFTNVBMFY6I7BCAC LSYLEVBDBZBGLSCXTRBW46WT4TUMMSPCH7M6HSNYI5SIH2WNPYEAC OI4FPFINEROK6GNDEMOBTGSPYIULCLRGGT5W3H7VLM7VFH22GMWQC OCA2NNDNXAHZGHO3TDQVLJSXKYPHMOORTKSOSSWUX3XSTULSDIRQC GCEF4N3VW2JFTWVXU2ND5XA63BNTMEGRBQQXYA3HULAKGYOYJP7AC KZ5GAYRPWF2BA5VEIW3A4G2TULATBL7YEDGFJU42GBP5DET7BI3AC 5OALPNN3FGDKFM4K5EQZV6FU6GCKHEVSJDXM6XFFC7LGXES7GLWQC MXA3RZYKUI4UF2ISY7JEF6VKX6NOPZMZH5SLLCZHRJKFIXXXDPSAC YPHKZVWM2FS7U3VNVDXFRJTBF4RLQ6K7ZWISLHOQJPYSKBELHFEAC 2CH77LZCSHAKRKLCCJGDGECVYFNCEV23NF3PFXHAQ2E33AJGSNVAC 2LOQ5ALJYHWSMU7ROSKD66BYGMK3O6HYNUQMGCZVKTRDOLEI75NQC 3KTBDIEU6TMFXZFVKAIHPARBN2BD5IQVUGBBSTTVYMD6S6SLMWCAC NPU5TTOXP2YIHHCCBXLLDHBBCC2TAINCBOZUMCRTPHM4WXQHBUKQC NHNP76LGNIVNIDMSDILAKEVSWFQ4LKNCYXVQEGKKJ75TSRPEBVEQC 2TQR4PSY2FBIKEEKC2Y5ZPVPOD2QJ3EATII47PPWNMTAQA7EQ6GAC GNQC72UXBU6KYXW6MXLNRGTLXV2VPQXMVCLYMJT6POTFXSF5ASJAC WLJCIXYMSTCNSYCFOEBQNDLBZ5D2Z3WTF4E4WYL5CFGIJ434FKNQC V5MJRFOZRVVDCPOWTLXPHS2HZBZKOOCPPKFMRP6MWZN6N62QLFAAC TACI4LU622F462UCH5ZKSFT24NFT7EBA7GBXSHZ24IN2LEQOZNQAC 2LEXWUW3EQUVE6IZTQROWRU6GDKZER35TGVDL3HFYYSEMA2RZEHAC GGJEDJOOKE5LM5KERQOWLM5FRIJGIF5UZBFPGY4F4MWBHY6Y5YUQC PRYVVWOS47BUSXKAROAKMCOBAU7J4344YR5EOACFLIUL2JKBMVCQC 76TK2E3QZ3CWH3VOQVI7SSZ3LN5LTTQQS26Y6YUSMVC7BDU4ZKZAC 6NYMNNADRZEZWL3ISZ2I7N7DFLZPFRGGLWPO25TDA7QLVX52HWQQC SVJZZDC3K6AKAXHGRNAZKRE2ZXEKJANNLG7LSSUZJARFBL5F7C4AC TRNWIQN6RPLDLYWULLKG5L255E7E3DPNGLCSLAF6IJWYQRCCLARQC MDXGMZU2MBEDMTB755D3RRYEFKF54GTTYTI5XJYKKKN5ZFQWZXTAC PYGMASTVHDTGX3LDTL364UWXEHVSWQ7STAJLZZI5YY6EA6EEICOAC PR4KIAZDOBQMEUOV2G7ZEZUW3E4L5ZCHYSS7PTYWGXPSNVRAGHCAC 2EKE4XLLUF44XPHJOJ53SAUP3TUDM572HWXMHJ5UVIYIQICRNEVAC KV7GGVERB4IOIWQJUK2RYBZZEUDMLCAUV3DIX7J7JISDCYRIQCGAC GSL4IZTVIXWUPG4W7TSJ7K43S4QYGADIJMGFKGZ3BRSYMILUO2RQC ELJNEPW26FUIIFY6D24274J7KZICRLE3TJHCFNRVLR5NZBNNV37AC 62PZGSUCEXJOCVWEOOENSDJITJFR27BGW7BPGFYVD3E5M6446RQQC F65ADDGLR2PNXVSM2XBHM3OSLQC2OTRR3GQBI7DJWIKPJCJ5CSOAC PHQPLJUQZOYZ7B3IDADDANMVXLKIKTU5DRSSEWTSDYCSDKX7M7JAC HYEAFRZ2UEKDYTAE2GDQLHEJBPQASP2NDLMXB7F6MTVK2BKOXKEAC BOFNXP5GZDCUMQG3LQVTSSFEQP7REQ4RIRJLDLETFSAGFTVDVEKAC EM276IH3NFWRAVNH4MLH43KOEXPZSX4K35CNVRSI3NGZHHUZQPBQC LE2ENPIOGEFZOSLJDXM6TUCC6CK5KAWMLYTMUMZKBPUPHP3HMR4QC K3M447T3CSGWDNTPKVYK47JPDCS4S4WLTKEKWPNMIDN6TCAIQULAC T57DTBX6J7E7FVEX4LQWXNKR7YXIHJW4HBCHUOYA5PTJUOEYHTEAC KCLRP4VWT2PDYLEG6DB6DYJRC2U7X4ZFCG3NGBOSFOYV7VPNRGZQC DFSDPDO7RHOLPVT4TD2Z3YZCKS6737LYIWBTJJI4BO73IIAJ5BYQC 5Q6NIG66SI7CS33S2TVIWSLLICWVAINELJJTMNR2UBWVZPGB7DZAC IMEJA43L3OX7S5KIYLZJ4F3ITACLAA5SZBHSCIJMULCPRSW7LXBAC KURLAXXIKHKBL7UDFVIR26BI5FDO3ZFLIQASKVGJRDD3RETTOWNQC EAEGCJV5JOW46KCZKKPBFKZ4Z3SDB3X4R7TLNXFWCIQN5UCNSXFQC Y72J4AZ37PHU3Q2YVE7LV7WAD5L2TVFSIAV3QEOBIYVWKDBQX3SAC 3OKKTUT4Q7W44JHILOFV5BVUA7ZOBIHBCEXGZ65CPXV4PRLI2W4QC 2ENZW7TVCS47BWCA4AIEVGKGMT4Y2TSM5IJ7O5K2VSWNXIN3SG4QC AYE2VEGJ63AWWX76SFQZLOTBIZOQRWBG4AZMIOSVOI2WZVRQJXYAC MP2TBKU6CNDMZKENYMBV62F5KQ27ZWEVPVRFS2RESVDQQT2IRR4AC LWTLEHXFIC3DYOHZJ2DXIOAZZSU67WHO2F4XCR4R76E3MAK3UUMQC R53OF3ONKT5VL5BGK63YSN6GXIIAVNYDG4UMHITK72WXFWPJ25MQC WOXIYUTL4NU7ACHQYXEXJDSXCRDLQ2X457KO6C7GEXFQZ43F3L7QC YJJ4X4JGABMVA5JBQW5UAWI543P3Y7NDVFTOHA6LIDA5KSFGUFNQC QLTJG7Q33ABBTDJ55K3OPLNSYBFBIVRS3UABXEY73RHYMOOJ542QC FLX77WI6PVTKLACN2LDU4RGX5CHM5YGOE4SVQQMAKGJTBYXD7F7QC PLKNHYZ4KXWWKC2DHXCI4WVO23I7VMEVYT5H2J6JDE4S3D3CHDJQC 6K5PFF6XBFTM6CXUVVFIH4CQMCMPHTND3ICDMRMNOME5BUBF27NQC MCSOCEE3NAGPXBLALPXA4EHXSWNC4AQ7M2O2KNZ3POOGZLTKGIBAC ODLKHO7BO2AODYO2OEQ6D4NSNBT5GR3CKLUXWMDLRYXL7DJOI7BAC 37ZVW7SWNUU3ZAE3MRPNJZIOPCHQL6UCN4T4PKSF5AHGIZNVILXAC E4HEHLRTRRIZZV4UMGVPG3LU4KJIPO5WFCBXHK7TG6YDURIEBVJQC EGH7XDBKE3R74VXLNTCAP5LJTRBPFUEMPS647MJARDGCMUHJG2QQC WZFMGVDTNVWZUX46RMEP5XHRZRBKL5G262VAQUWP5AYY7TRXN5XAC 62KGT45CQ5IFVVDNYMQWCZ57ZIVWBG6W6TB3MISHKJZGHJQBUXHAC AIRIP35Z6BPIFYJUDGXTWJICTVHAMQFZHXLWI32I2VYWB24H6Y6QC U7M4M2F7P5TGLTHKQ7J72GQFNPBII4PLJVJ44YVVOYEI4KPUDI6AC BTKAW76LJFOXLINKJKOIK47MUDFHZKDMWX3NQODS2XUQLYGOZXUQC 242L3OQXTU2TCAINRJXQEEDSXQXM7Y7USUPBK37ZNM3A7V5TUDSAC DXT4QTAH5G6J7ZB3SMOOXVECKWYUPZVE2ODMUFTPPNHLTOSZLQSAC 325NPKHMTENMI5CJKBLI2KZJ6TRBJEIGVUM4P22OPAJM3TALFUGAC 5Z7WU65HS47ABW62BBE4AEYZNR44O4KT6RBN2TIYXJW4577EB5RQC UDQBOVKKGKAYBTGCEOM33P3XXVJIOGO4DPKTDUBQDOCLQJKAFEYQC HAHDW2HMQPOQ3EWE4W6YSWPJNKBUPABE3OWMXGAEAL2DN6HJTNVAC U52E2XZNDEMIX5QJC6TREX5BSLNYG23Y4XQVFFKS6OFB2KIBW7BAC CG5PH4DWFEUEICYJAPPGAMDCCTOGUB7475IC5IFKSB4ADL2KCJZQC CPZGQT72EBP3SEDBPDWQRK5IUGA664PHXNP2GOHJLP43PKPWF25AC 5L7K4GBDEAFH44LMLNKVFMHLWDNXXBKRPEI347VE5ZLXVFSMD2FAC J6WEC2D6QSURWOJZB4YRTXQQAHFPAAEL55OPQU4ZH6M6LUKYJPJAC 5FW7YOFTLKHRND6IOR4HG4X3C5BO2WV5KTEUW3PPKCRU5L5GXKXQC GTZLDCKDIA2RQTTZMNCHSP7USVFEOMOJNUH7WKZW7YRJ7LXNWYOAC XLG3ODIKY5BWRLBMDKXRUDC5GVAKNA4GUS7CQ7UJJHYRNXWHB6CAC FKNXK2OAH4U2V2TXCHWE4C3Z5DROBIIPSXUWFKP7Q3DSNOKFFL5AC 52ZZ5TIEK5Z6VVXO7R3EFV5JTIWDA4IDD3YISXHHTHQMOAKVYJJQC KECEMMMRW2VVBZ567HJQPGLC57LTSBKWH7UFP32IW43D23X6WTEQC QYIFOHW3WDDQMK4ATY6IOSQRFHJOQ5QCPDKRC4GVGWLQEH4HGWVQC I3ZXLS6BR2AYYRDCFM57J64A7TQR3M7IJFDWDHB25FVAUOLV3L4AC 2BN6PABGBUXTB533VXW3MXAQI6IHFKUQ3PDSUHHOURUKQNDHM3TQC SIH25NMC63DINWY3EBHDITTOPBABJODM7D5ZOG4CVC7GGDCX5S3AC 3ZSUBI574IYW3BKS6OFPDD6UY2IJBNOQIGA4YFGQSF4VZ3PPATYQC 2CTN2IEF4ZCVZQORAEBXAUDANF6NYZA24GQ5PXK2WUDWYU5UV25QC BW2IUB3KA4AKD35DYLCUCUM4Z32FMKGZNUBQBAEDIQJJYPA547MAC JOPVPUSAMMU6RFVDQR4NJC4GNNUFB7GPKVH7OS5FKCYS5QZ53VLQC VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC SPNMXTYRSNPNQJNBTYDZSHYDZVZRPM4LI5QX7GR2TLTC6SPJX4DAC LA62RA7SUK6ADGTKNZ7PSDKHMWHOJB2YTOTFG3COUV7YYTA6YNUAC PX3736DXTMOKZHWGZKY446VJQ4URULLXZTG34A3JYZRDWWE5ETKAC AD34IX2ZSGYGU3LGY2IZOZNKD4HRQOYJVG5UWMWLXJZJSM62FFOAC YBRUEETZG2ACA4EIRARTH225666R2KPV22ZZO6JI2OO2P4N2MP5QC EMBBTRXDTL6DYNYUHOIZ2BXRCBS3BM6BPPZ4YLJAONG4K7DRSKNAC SGPI6HF2XIOCLOYRPVXSKVGFWAAZWEFBEGZX5RYGHCPXLG6W6SSQC WNL5Z7EKPNBDZM6ACJOBL725XLQDQMDG6D276J257UJTROBFJH4QC 3KTHONHVR2N3V3PTAN2LN3SVWXJ55NQ45GINB2CKJ7UWPPVZJWLQC 2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC 6QTCZLJ7E6F3UGS6LFH2FMKPFNKJ7PDWHHI2EWTK7SB2K4IBY5SQC UWPRFECMDHR6H7AOLZ7BKT5TG7LIRZOO425KI3UZ3MLGNKVDJGGAC AZQMREI6PRUAJKPIIZRLSVC4YJZ544TSIQ5OFHRRXVE3IMIYFMAQC 32V6ZHQBHMVAY66WO5FAHXPY6W6PWNAURIRNN3S63YUCL5LCH4LAC 5PRIAZWT3XQZNBRJSDB2IQ3ADGL6KBSBS7QQ6XMGA553VPYFLTPQC WJBZZQE4A4KLYGS2KA254I6VN2DVXDY4XKCNAE76GTMLLQGYCUOQC PJEQCTBL2ZX5Q7NJQ3KCWSHHBV2QWGYS5NVMRDNK5LOOIIRRIPHAC 6D5MOJS4KEFIOKCBR37NXQHAJSHFIWUALGQ6DTSKWZOPS4TBIQJAC 5ZA3BRNYWKSGEBJ4JLA4UBC3LJPT5JBWYCU7PQYRSGX6MJMEWDIQC PV2YA7KSWRCOKDS2WYO45WKE5L3CK56HPYT6DRVQRI3ZIE3B633AC YEI2GCL24TW7C7OAMCXJVKWIUK27O6JIA3OGM6CD2C5C3OBJDKZQC WLWNS6FBT6D3HKOFWDPBKLK7KS73LJJLWLHNWX3YJ723OHJBZGDQC SJJTCVWI4FFGF22FAUDMEQAVOZ2IHL6LUYTQBWTEZ3DTOGPT4FCAC 5UKUADTWMNWPOPBBTXUXY7UNFW64DWANI2RQHKSCSZNWHTQM4GUAC H6QZ7GRR2UBK4POXP6PNMYHHW54SJAQYTTFVPQK4YIJZ4FECAPPQC 2Y7YH7UPQWDNYDJN4BYY2MOHA36B2BIRX6DMIAKHJPQC7UP2R6NQC WLG7OCV77KWWYLBI3DMCCEJ5YOBCTSV5DD5OPH2K4UBCNWKFN5IQC BK45KKP4YYNNEL3UOZGUE3A5GDL76LTFIUQ4OZQIA3AMYPB7RPIAC K6DTOGOQG4YXWTFKHGZ3DIC23G5ZV3WQZQPRRDYIWEGLNQW6QDGQC PP2IIHL6EK4HBFFSYAQNV35BKIK6D4EL2JQOY7NZVJX2DXCLSMGAC ERQKFTPVWZO4WJD2WRIV33JWTWZSF4HNTK2GD7QT5I5TIL3SOGKQC QSPYRABWJDUVTRV5HTCOC4FY3DGYPLQU4DJ2UDJLPIIJWMTPOA3AC FXI74QCLOZ4BS7UVZ3U2PE3LOL7MX3FWGHZCTGH3DYFXGTXVVIRAC ANVLB2TX6ODOEUY7OADTQG47W7DNRHD6NXUPIJR5LVSJFMFJ3L2AC U46N4W3QLSD5F7DNP6VB43CEE3OA2PJFLGNUG3MZ7EMNTFSUIP6AC 3LWDKR5FPZNW2ESSTTLWBBEBJ3B77UH4NEOC26PHOVG4HKOU4VQAC OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC DIAP63OYZA2Q5EJN767BNV7VORLASO5T6MNESDFCNVTODU7Z36WQC KKMFQDR43ZWVCDRHQLWWX3FCWCFA3ZSXYOBRJNPHUQZR2XPKWULAC K464QQR4FTXFUMHFWAGOD5DJ6YHUBUKRHLXF2ORE74DVT7TVQ35QC 5YGO6JX5WTBNIRW2LKLJF7EE7Z5UNXAMLG24AUV5BK227S4AI4IQC GRSHSSV5ZRYOXD3SGMCOXKVFXLNZYLAN3YVBDUE6IVZBIK5S3UXQC AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC 3QNOKBFMKBGXBVJIRHR2444JRRMBTABHE4674NR3DT67RRM2X6GAC VDXOEDS757ISV5KM5AUFZDFH223CHQB4FK3TQOQ75CDDJAPLCFQQC PX7DDEMOBGPVK3FXKK5XEPG24CJXZSVW67DLG2JZZ5E77NVEAA3AC -- 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 operationsendendreturn resultendendreturn resultendend-- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.-- compare with App.initialize_globalslocal event = {lines={},-- no filename; undo history is cleared when filename changes}-- deep copy lines without cached stuff like text fragmentsendreturn eventend-- 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)endfunction 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, {})endendfor i=s,e dolocal line = State.lines[i]table.insert(event.lines, {data=line.data, dataB=line.dataB})start_line=s,end_line=e,screen_top=deepcopy(State.screen_top1),selection=deepcopy(State.selection1),cursor=deepcopy(State.cursor1),-- Snapshot everything by default, but subset if requested.e = sendif s < 1 then s = 1 endif e < 1 then e = 1 endif e > #State.lines then e = #State.lines endif s > #State.lines then s = #State.lines endassert(#State.lines > 0)assert(s)if e == nil thenfunction snapshot(State, s,e)-- Copy all relevant global state.function redo_event(State)if State.next_history <= #State.history then--? print('restoring history', State.next_history+1)local result = State.history[State.next_history]State.next_history = State.next_history+1function 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]function 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] = nil
-- major tests for text editing flowsfunction test_insert_first_character()io.write('\ntest_insert_first_character')App.screen.init{width=120, height=60}App.screen.check(y, 'a', 'F - test_insert_first_character/screen:1')endfunction test_move_left_to_previous_line()io.write('\ntest_move_left_to_previous_line')App.screen.init{width=120, height=60}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}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}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}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}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}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}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}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}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}check_eq(Editor_state.cursor1.pos, 8, 'F - test_skip_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}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')endEditor_state.lines = load_array{'abc def', 'ghi'}Editor_state.cursor1 = {line=1, pos=8}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()function test_skip_past_tab_to_next_word()io.write('\ntest_skip_past_tab_to_next_word')App.screen.init{width=120, height=60}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}check_eq(Editor_state.cursor1.pos, 9, 'F - test_skip_multiple_spaces_to_next_word')endEditor_state.lines = load_array{'abc def'}Editor_state.cursor1 = {line=1, pos=4} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc\tdef'}Editor_state.cursor1 = {line=1, pos=1} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Editor_state.cursor1 = {line=1, pos=4} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Editor_state.cursor1 = {line=1, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi'}Editor_state.cursor1 = {line=2, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Editor_state.cursor1 = {line=1, pos=6} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def\tghi'}Editor_state.cursor1 = {line=1, pos=10} -- within third wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Editor_state.cursor1 = {line=1, pos=5} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Editor_state.cursor1 = {line=1, pos=4} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Editor_state.cursor1 = {line=1, pos=3}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Editor_state.cursor1 = {line=1, pos=4} -- past end of lineedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'right')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Editor_state.cursor1 = {line=2, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'left')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()function test_draw_text()io.write('\ntest_draw_text')App.screen.init{width=120, height=60}App.screen.check(y, 'abc', 'F - test_draw_text/screen:1')App.screen.check(y, 'def', 'F - test_draw_text/screen:2')App.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}App.screen.check(y, 'abc', 'F - test_draw_wrapping_text/screen:1')y = y + Editor_state.line_heightendfunction test_draw_word_wrapping_text()io.write('\ntest_draw_word_wrapping_text')App.screen.init{width=60, height=60}App.screen.check(y, 'abc ', 'F - test_draw_word_wrapping_text/screen:1')App.screen.check(y, 'def ', 'F - test_draw_word_wrapping_text/screen:2')App.screen.check(y, 'ghi', 'F - test_draw_word_wrapping_text/screen:3')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')endcheck_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:pos')function 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}App.screen.check(y, 'abcd ', 'F - test_draw_text_wrapping_within_word/screen:1')y = y + Editor_state.line_heightendfunction 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}y = y + Editor_state.line_heighty = y + Editor_state.line_heightendfunction test_edit_wrapping_text()io.write('\ntest_edit_wrapping_text')App.screen.init{width=50, height=60}App.screen.check(y, 'abc', 'F - test_edit_wrapping_text/screen:1')y = y + Editor_state.line_heightendfunction test_insert_newline()io.write('\ntest_insert_newline')App.screen.check(y, 'abc', 'F - test_insert_newline/baseline/screen:1')App.screen.check(y, 'def', 'F - test_insert_newline/baseline/screen:2')App.screen.check(y, 'ghi', 'F - test_insert_newline/baseline/screen:3')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')App.screen.check(y, 'a', 'F - test_insert_newline/screen:1')App.screen.check(y, 'bc', 'F - test_insert_newline/screen:2')App.screen.check(y, 'def', 'F - test_insert_newline/screen:3')endfunction test_insert_from_clipboard()io.write('\ntest_insert_from_clipboard')App.screen.check(y, 'abc', 'F - test_insert_from_clipboard/baseline/screen:1')App.screen.check(y, 'def', 'F - test_insert_from_clipboard/baseline/screen:2')App.screen.check(y, 'ghi', 'F - test_insert_from_clipboard/baseline/screen:3')App.clipboard = 'xy\nz'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')App.screen.check(y, 'zbc', 'F - test_insert_from_clipboard/screen:2')App.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}endfunction test_pagedown()io.write('\ntest_pagedown')App.screen.init{width=120, height=45}-- initially the first two lines are displayedApp.screen.check(y, 'abc', 'F - test_pagedown/baseline/screen:1')App.screen.check(y, 'def', 'F - test_pagedown/baseline/screen:2')-- after pagedown the bottom line becomes the topcheck_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')App.screen.check(y, 'ghi', 'F - test_pagedown/screen:2')endendfunction test_down_arrow_moves_cursor()io.write('\ntest_down_arrow_moves_cursor')App.screen.init{width=120, height=60}-- initially the first three lines are displayedApp.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/baseline/screen:1')App.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/baseline/screen:2')App.screen.check(y, 'ghi', 'F - test_down_arrow_moves_cursor/baseline/screen:3')-- after hitting the down arrow, the cursor moves down by 1 linecheck_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 unchangedApp.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/screen:1')App.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/screen:2')App.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}App.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:1')App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:2')App.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 linecheck_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')App.screen.check(y, 'ghi', 'F - test_down_arrow_scrolls_down_by_one_line/screen:2')App.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 lineEditor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:1')App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:2')App.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 linecheck_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')y = Editor_state.topcheck_eq(Editor_state.cursor1.pos, 5, 'F - test_down_arrow_scrolls_down_by_one_screen_line/cursor:pos')App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:1')App.screen.check(y, 'ghi ', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:2')App.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 lineEditor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:1')App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:2')-- after hitting the down arrow the screen scrolls down by one linecheck_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')y = Editor_state.topcheck_eq(Editor_state.cursor1.pos, 5, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/cursor:pos')App.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_heightendfunction 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')Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'abc', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:1')App.screen.check(y, 'def', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:2')-- after hitting pagedown the screen scrolls down to start of a long linecheck_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 upy = y + Editor_state.line_heighty = 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}App.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/baseline/screen:1')App.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/baseline/screen:2')App.screen.check(y, 'ghi', 'F - test_up_arrow_moves_cursor/baseline/screen:3')-- after hitting the up arrow the cursor moves up by 1 linecheck_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 unchangedApp.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/screen:1')App.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/screen:2')App.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}App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:1')App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:2')App.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 linecheck_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')App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/screen:2')App.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 lineEditor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/baseline/screen:1')App.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 lineApp.screen.check(y, 'ghi ', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:1')App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:2')App.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:3')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 lineEditor_state.lines = load_array{'abc def', 'ghi', 'jkl', 'mno'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:1')App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:2')App.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 lineApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:1')App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:2')App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:3')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}App.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:1')App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:2')App.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 linecheck_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 lineApp.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:2')App.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}-- initially the last two lines are displayedApp.screen.check(y, 'def', 'F - test_pageup/baseline/screen:1')App.screen.check(y, 'ghi', 'F - test_pageup/baseline/screen:2')-- after pageup the cursor goes to first linecheck_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')App.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 lineEditor_state.lines = load_array{'abc def', 'ghi', 'jkl', 'mno'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:1')App.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:2')App.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 topcheck_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')App.screen.check(y, 'def', 'F - test_pageup_scrolls_up_by_screen_line/screen:2')App.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')App.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_from_middle_screen_line/baseline/screen:2')App.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 topcheck_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')App.screen.check(y, 'def', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:2')App.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 lineEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'abc', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:1')App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:2')App.screen.check(y, 'ghi', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:3')-- after hitting the enter key the screen scrolls downcheck_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')App.screen.check(y, 'g', 'F - test_enter_on_bottom_line_scrolls_down/screen:2')App.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 screenEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'jkl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/baseline/screen:1')-- after hitting the enter key the screen does not scroll downcheck_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')App.screen.check(y, 'kl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:2')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.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:1')App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:2')App.screen.check(y, 'xyz', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:3')-- add to the line until it's wrapping over 3 screen linesApp.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:1')App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:2')App.screen.check(y, 'stu', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:3')-- try to move the cursor earlier in the third screen line by clicking the mouse-- cursor should moveendfunction 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}App.screen.check(y, 'def', 'F - test_backspace_can_scroll_up/baseline/screen:1')App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/baseline/screen:2')App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/baseline/screen:3')-- after hitting backspace the screen scrolls up by one linecheck_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')App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/screen:2')App.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 lineEditor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:1')App.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:2')-- after hitting backspace the screen scrolls up by one screen liney = y + Editor_state.line_heighty = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/screen:3')endfunction test_backspace_past_line_boundary()io.write('\ntest_backspace_past_line_boundary')-- position cursor at start of a (non-first) lineEditor_state.lines = load_array{'abc', 'def'}Editor_state.cursor1 = {line=2, pos=1}Text.redraw_all(Editor_state)-- backspace joins with previous lineendfunction test_undo_insert_text()io.write('\ntest_undo_insert_text')App.screen.init{width=120, height=60}-- insert a charactercheck_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')App.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')App.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2')App.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3')-- undocheck_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')App.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2')App.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}-- delete a charactercheck_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')App.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2')App.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3')-- undo--? -- after undo, the backspaced key is selectedcheck_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')App.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2')App.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')function 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}-- 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_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')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')endEditor_state.screen_top1 = {line=1, pos=1}Editor_state.lines = load_array{'abc', 'def', 'ghi', 'deg'}endy = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'C-z')y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'backspace')Editor_state.lines = load_array{'abc', 'defg', 'xyz'}Editor_state.cursor1 = {line=2, pos=5}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'C-z')y = y + Editor_state.line_heighty = y + Editor_state.line_heightlocal y = Editor_state.topedit.draw(Editor_state)edit.run_after_textinput(Editor_state, 'g')Editor_state.lines = load_array{'abc', 'def', 'xyz'}Editor_state.cursor1 = {line=2, pos=4}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()edit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary")App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()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')App.screen.check(y, 'kl', 'F - test_backspace_can_scroll_up_screen_line/screen:2')edit.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.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'backspace')y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()check_eq(Editor_state.cursor1.line, 1, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:line')check_eq(Editor_state.cursor1.pos, 26, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos')edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)y = y + Editor_state.line_heighty = y + Editor_state.line_heighty = Editor_state.topedit.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 = y + Editor_state.line_heighty = y + Editor_state.line_heightApp.screen.init{width=100, height=200}Editor_state.lines = load_array{'abc def ghi jkl mno pqr ', 'xyz'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()function test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom()io.write('\ntest_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom')-- display just an empty bottom line on screenEditor_state.lines = load_array{'abc', ''}Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)Text.redraw_all(Editor_state)-- after hitting the inserting_text key the screen does not scroll downcheck_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')endedit.run_after_textinput(Editor_state, 'a')App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()function test_typing_on_bottom_line_scrolls_down()io.write('\ntest_typing_on_bottom_line_scrolls_down')-- display a few lines with cursor on bottom lineEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'abc', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:1')App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:2')App.screen.check(y, 'ghi', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:3')-- after typing something the line wraps and the screen scrolls downcheck_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_heightendfunction 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 lineEditor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)-- cursor is at top of screenApp.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/baseline/screen:1')App.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 lineApp.screen.check(y, 'ghi ', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:1')App.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:2')App.screen.check(y, 'mno', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:3')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 lineEditor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)-- cursor is at bottom right of screenApp.screen.check(y, 'abc', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:1')App.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:2')App.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 linecheck_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')App.screen.check(y, 'ghi ', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:2')App.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 lineEditor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)-- cursor is at top of screenApp.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/baseline/screen:1')App.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 lineApp.screen.check(y, 'ghi ', 'F - test_home_scrolls_up_in_wrapped_line/screen:1')App.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/screen:2')App.screen.check(y, 'mno', 'F - test_home_scrolls_up_in_wrapped_line/screen:3')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 lineEditor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)-- cursor is at bottom right of screenApp.screen.check(y, 'abc', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:1')App.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:2')App.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 linecheck_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')App.screen.check(y, 'ghi ', 'F - test_end_scrolls_down_in_wrapped_line/screen:2')App.screen.check(y, 'jkl', 'F - test_end_scrolls_down_in_wrapped_line/screen:3')endy = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'end')y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()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')y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'home')y = Editor_state.topy = y + Editor_state.line_heightEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'right')y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()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')y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'left')y = Editor_state.topy = y + Editor_state.line_heightEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()App.screen.check(y, 'kl', 'F - test_typing_on_bottom_line_scrolls_down/screen:3')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_typing_on_bottom_line_scrolls_down/screen:2')edit.run_after_textinput(Editor_state, 'j')edit.run_after_textinput(Editor_state, 'k')edit.run_after_textinput(Editor_state, 'l')y = y + Editor_state.line_heighty = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'return')App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'return')y = y + Editor_state.line_heighty = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'pageup')y = y + Editor_state.line_height-- display a few lines starting from the middle of a line (Editor_state.cursor1.pos > 1)Editor_state.lines = load_array{'abc def', 'ghi jkl', 'mno'}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.topText.redraw_all(Editor_state)App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'pageup')y = y + Editor_state.line_heighty = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'pageup')y = y + Editor_state.line_heightedit.draw(Editor_state)local y = Editor_state.topEditor_state.lines = load_array{'abc', 'def', 'ghi'}Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'up')y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.lines = load_array{'', 'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()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')y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'up')y = Editor_state.topy = y + Editor_state.line_heighty = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()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')y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'up')y = Editor_state.topy = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'up')y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heighty = Editor_state.topedit.run_after_keychord(Editor_state, 'up')y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()App.screen.check(y, 'kl', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:2')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')y = Editor_state.topApp.screen.check(y, 'ghij', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:1')check_eq(Editor_state.cursor1.pos, 5, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/cursor:pos')edit.run_after_keychord(Editor_state, 'down')edit.run_after_keychord(Editor_state, 'pagedown')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')y = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()App.screen.check(y, 'kl', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:3')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')edit.run_after_keychord(Editor_state, 'down')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')y = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'down')y = y + Editor_state.line_heighty = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'down')y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heighty = Editor_state.topedit.run_after_keychord(Editor_state, 'down')y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.draw(Editor_state)local y = Editor_state.topEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()function 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_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 lineEditor_state.lines = load_array{'abc def ghi jkl mno pqr stu vwx yza bcd efg hij', 'XYZ'}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.topText.redraw_all(Editor_state)App.screen.check(y, 'abc ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:1')App.screen.check(y, 'def ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:2')App.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 linecheck_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, 'mno ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:3')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:2')edit.run_after_keychord(Editor_state, 'pagedown')y = y + Editor_state.line_heighty = y + Editor_state.line_heightApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'pagedown')y = y + Editor_state.line_heightedit.draw(Editor_state)local y = Editor_state.topEditor_state.lines = load_array{'abc', 'def', 'ghi'}Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}check_eq(Editor_state.cursor1.line, 1, 'F - test_move_cursor_using_mouse/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_move_cursor_using_mouse/cursor:pos')edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cacheedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightedit.run_after_keychord(Editor_state, 'C-v')-- paste some text including a newline, check that new line is createdy = y + Editor_state.line_heighty = y + Editor_state.line_height-- display a few linesEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()endfunction test_insert_newline_at_start_of_line()io.write('\ntest_insert_newline_at_start_of_line')-- display a lineEditor_state.lines = load_array{'abc'}Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)-- hitting the enter key splits the linecheck_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')edit.run_after_keychord(Editor_state, 'return')App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heighty = Editor_state.top-- hitting the enter key splits the lineedit.run_after_keychord(Editor_state, 'return')y = y + Editor_state.line_heighty = y + Editor_state.line_height-- display a few linesEditor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}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.topText.redraw_all(Editor_state)App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()App.screen.check(y, 'fg', 'F - test_edit_wrapping_text/screen:3')y = y + Editor_state.line_heightApp.screen.check(y, 'de', 'F - test_edit_wrapping_text/screen:2')Editor_state.lines = load_array{'abc', 'def', 'xyz'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()function test_click_on_wrapping_line()io.write('\ntest_click_on_wrapping_line')-- display a wrapping lineEditor_state.lines = load_array{"madam I'm adam"}Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}App.screen.check(y, 'madam ', 'F - test_click_on_wrapping_line/baseline/screen:1')y = y + Editor_state.line_height-- click past end of second screen line-- cursor moves to end of screen lineendfunction test_click_past_end_of_wrapping_line()io.write('\ntest_click_past_end_of_wrapping_line')-- display a wrapping lineEditor_state.lines = load_array{"madam I'm adam"}Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}App.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line/baseline/screen:1')y = y + Editor_state.line_heighty = y + Editor_state.line_height-- click past the end of it-- cursor moves to end of lineend-- display a wrapping line containing non-ASCIIEditor_state.lines = load_array{'madam I’m adam'} -- notice the non-ASCII apostropheEditor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}y = y + Editor_state.line_heighty = y + Editor_state.line_heighty = y + Editor_state.line_height-- click past the end of it-- cursor moves to end of lineendfunction 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 lengthEditor_state.lines = load_array{'the quick brown fox jumped over the lazy dog'}Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}App.screen.check(y, 'the quick brown fox ', 'F - test_click_past_end_of_word_wrapping_line/baseline/screen:1')-- cursor moves to end of screen lineendcheck_eq(Editor_state.cursor1.pos, 20, 'F - test_click_past_end_of_word_wrapping_line/cursor')-- click past the end of the screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)y = y + Editor_state.line_heightedit.draw(Editor_state)local y = Editor_state.topText.redraw_all(Editor_state)App.screen.init{width=160, height=80}Editor_state = edit.initialize_test_state()-- 0 1 2-- 123456789012345678901check_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-pointsedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)App.screen.check(y, 'am', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:3')App.screen.check(y, 'I’m ad', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:2')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')Text.redraw_all(Editor_state)App.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234function test_click_past_end_of_wrapping_line_containing_non_ascii()io.write('\ntest_click_past_end_of_wrapping_line_containing_non_ascii')check_eq(Editor_state.cursor1.pos, 15, 'F - test_click_past_end_of_wrapping_line/cursor') -- one more than the number of UTF-8 code-pointsedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)App.screen.check(y, 'am', 'F - test_click_past_end_of_wrapping_line/baseline/screen:3')y = y + Editor_state.line_heightApp.screen.check(y, "I'm ad", 'F - test_click_past_end_of_wrapping_line/baseline/screen:2')edit.draw(Editor_state)local y = Editor_state.topText.redraw_all(Editor_state)App.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234function 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 lineEditor_state.lines = load_array{"madam I'm adam"}Editor_state.cursor1 = {line=1, pos=8}Editor_state.screen_top1 = {line=1, pos=7}Editor_state.screen_bottom1 = {}y = y + Editor_state.line_height-- click past end of second screen line-- cursor moves to end of screen lineendcheck_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')edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)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')Text.redraw_all(Editor_state)App.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234check_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')edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)y = y + Editor_state.line_heightApp.screen.check(y, "I'm ad", 'F - test_click_on_wrapping_line/baseline/screen:2')edit.draw(Editor_state)local y = Editor_state.topText.redraw_all(Editor_state)App.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234App.screen.check(y, '’m a', 'F - test_draw_wrapping_text_containing_non_ascii/screen:3')App.screen.check(y, 'am I', 'F - test_draw_wrapping_text_containing_non_ascii/screen:2')Editor_state.lines = load_array{'madam I’m adam', 'xyz'} -- notice the non-ASCII apostropheEditor_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')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()App.screen.check(y, 'ijk', 'F - test_draw_text_wrapping_within_word/screen:3')y = y + Editor_state.line_heightApp.screen.check(y, 'e fgh', 'F - test_draw_text_wrapping_within_word/screen:2')Editor_state.lines = load_array{'abcd e fghijk', 'xyz'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()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')y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.lines = load_array{'abc def ghi', 'jkl'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()App.screen.check(y, 'fgh', 'F - test_draw_wrapping_text/screen:3')y = y + Editor_state.line_heightApp.screen.check(y, 'de', 'F - test_draw_wrapping_text/screen:2')Editor_state.lines = load_array{'abc', 'defgh', 'xyz'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()y = y + Editor_state.line_heighty = y + Editor_state.line_heightEditor_state.lines = load_array{'abc', 'def', 'ghi'}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.topText.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()function test_click_with_mouse()-- display two lines with cursor on one of themApp.screen.init{width=50, height=80}-- click on the other line-- cursor movesendendfunction 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')endcheck_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_takes_margins_into_account/cursor:pos')function 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}-- click on the empty line-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_empty_line/cursor')edit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)Editor_state.lines = load_array{'', 'def'}Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()function 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')endcheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse/cursor:line')edit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)Editor_state.lines = load_array{'abc', 'def'}Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()io.write('\ntest_click_with_mouse')endfunction test_press_ctrl()io.write('\ntest_press_ctrl')-- press ctrl while the cursor is on textApp.screen.init{width=50, height=80}endfunction test_move_left()io.write('\ntest_move_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}check_eq(Editor_state.cursor1.pos, 2, 'F - test_move_right')Editor_state.lines = load_array{'a'}Editor_state.cursor1 = {line=1, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'right')Text.redraw_all(Editor_state)Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'a'}Editor_state.cursor1 = {line=1, pos=2}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'left')Text.redraw_all(Editor_state)App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{''}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')Text.redraw_all(Editor_state)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.topEditor_state = edit.initialize_test_state()function test_initial_state()io.write('\ntest_initial_state')App.screen.init{width=120, height=60}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')endendfunction 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')Editor_state.lines = load_array{}Text.redraw_all(Editor_state)edit.draw(Editor_state)Editor_state = edit.initialize_test_state()
Text = {}endendendend-- 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 = startposfor _, f in ipairs(line_cache.fragments) 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 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)endText.draw_cursor(State, x+Text.x(frag, State.cursor1.pos-pos+1), y)endendendx = x + frag_widthendpos = pos + frag_lenendreturn false, x,y, pos, screen_line_starting_posendlocal line = State.lines[line_index]local line_cache = State.line_cache[line_index]local screen_line_starting_pos = startposlocal pos = 1for _, f in ipairs(line_cache.fragmentsB) doText.compute_fragmentsB(State, line_index, x)function Text.draw_wrapping_lineB(State, line_index, x,y, startpos)App.color(Text_color)elseif Focus == 'edit' thenif State.cursor1.pos and line_index == State.cursor1.line thenselect_color(frag)App.color(Text_color)local frag, frag_text = f.data, f.texty = y + State.line_heightif y + State.line_height > App.screen.height thenendscreen_line_starting_pos = posendendendendendpos = pos + frag_lenendreturn false, x,y, pos, screen_line_starting_posendendfunction Text.draw_cursor(State, x, y)-- blink every 0.5sif math.floor(Cursor_time*2)%2 == 0 thenendState.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.drawline_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_lenendText.compute_fragments(State, line_index)App.color(Cursor_color)love.graphics.rectangle('fill', x,y, 3,State.line_height)local x = State.left-- try to wrap at word boundariesendendif #frag > 0 thenendx = x + frag_widthendendif App.mouse_down(1) then return endif App.ctrl_down() or App.alt_down() or App.cmd_down() then return end--? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)endrecord_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})endend-- Don't handle any keys here that would trigger love.textinput above.if chord == 'return' thenText.insert_return(State)Text.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)})if State.cursor_y > App.screen.height - State.line_height thenelseif chord == 'tab' then--? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)endelseif chord == 'backspace' thenif byte_start thenif byte_end thenelseendendState.cursor1.line = State.cursor1.line-1endtop2 = Text.previous_screen_line(State, top2, State.left, State.right)endelseif chord == 'delete' thenendlocal 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 State.cursor1.pos and State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenif byte_start thenif byte_end thenelseendendtable.remove(State.lines, State.cursor1.line+1)end--== shortcuts that move the cursorelseif chord == 'left' thenelseif chord == 'right' thenelseif chord == 'S-left' thenelseif chord == 'S-right' thenText.right(State)Text.left(State)-- C- hotkeys reserved for drawings, so we'll use M-elseif chord == 'M-left' thenelseif chord == 'M-right' thenelseif chord == 'M-S-left' thenelseif chord == 'M-S-right' thenText.word_right(State)Text.word_left(State)elseif chord == 'home' thenelseif chord == 'end' thenelseif chord == 'S-home' thenelseif chord == 'S-end' thenelseif chord == 'up' thenelseif chord == 'down' thenelseif chord == 'S-up' thenelseif chord == 'S-down' thenText.down(State)elseif chord == 'pageup' thenelseif chord == 'pagedown' thenelseif chord == 'S-pageup' thenelseif chord == 'S-pagedown' thenText.pagedown(State)endendfunction Text.insert_return(State)State.cursor1 = {line=State.cursor1.line+1, pos=1}if State.cursor1.pos then-- when inserting a newline, move any B side to the new linelocal byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)table.insert(State.lines, State.cursor1.line+1, {data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB})table.insert(State.line_cache, State.cursor1.line+1, {})State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)State.lines[State.cursor1.line].dataB = nilText.clear_screen_line_cache(State, State.cursor1.line)State.cursor1 = {line=State.cursor1.line+1, pos=1}else-- disable enter when cursor is on the B sideendend-- duplicate some logic from love.draw--? print(App.screen.height)top2 = Text.previous_screen_line(State, top2)--? print(y, top2.line, top2.screen_line, top2.screen_pos)y = y - State.line_heightif State.screen_top1.line == 1 and State.screen_top1.pos and State.screen_top1.pos == 1 then break endlocal y = App.screen.height - State.line_heightwhile y >= State.top dolocal top2 = Text.to2(State, State.screen_top1)--? print('pageup')function Text.pageup(State)Text.pageup(State)end--? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)--? print('pageup end')Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)State.screen_top1 = Text.to1(State, top2)State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}end--? print('top now', State.screen_top1.line)end--? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)if screen_line_starting_pos == 1 then--? print('cursor is at first screen line of its line')-- line is done; skip to previous text lineif State.cursor1.line > 1 then--? print('found previous text line')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]State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1endelse-- move up one screen line in current lineassert(screen_line_index > 1)--? print('cursor pos is now '..tostring(State.cursor1.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)endend--? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)if Text.cursor_at_final_screen_line(State) then-- line is done, skip to next text line--? print('cursor at final screen line of its line')if State.cursor1.line < #State.lines then--? print(State.cursor1.pos)State.cursor1 = {line = State.cursor1.line+1,}pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line+1].data, State.cursor_x, State.left)function Text.down(State)function Text.upB(State)local line_cache = State.line_cache[State.cursor1.line]local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)assert(screen_line_indexB >= 1)if screen_line_indexB == 1 then-- move to A side of previous lineif State.cursor1.line > 1 thenState.cursor1.line = State.cursor1.line-1State.cursor1.posB = nillocal prev_line_cache = State.line_cache[State.cursor1.line]local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos]local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos)local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset)State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1endelseif screen_line_indexB == 2 then-- all-B screen-line to potentially A+B screen-linelocal xA = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_paddingif State.cursor_x < xA thenState.cursor1.posB = nillocal 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) - 1elselocal 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-linelocal 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)endendText.populate_screen_line_starting_posB(State, State.cursor1.line)Text.populate_screen_line_starting_posB(State, State.cursor1.line)Text.populate_screen_line_starting_pos(State, State.cursor1.line)Text.populate_screen_line_starting_pos(State, State.cursor1.line)-- 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 lineif 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)endState.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]State.cursor1 = {line=State.cursor1.line-1, pos=nil}function Text.up(State)if State.cursor1.pos thenText.upA(State)elseText.upB(State)endendfunction Text.upA(State)--? print('pagedown end')Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks--? print('pagedown')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)Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos}local bot2 = Text.to2(State, State.screen_bottom1)local new_top1 = Text.to1(State, bot2)function Text.pagedown(State)Text.pagedown(State)State.selection1 = {}Text.pageup(State)State.selection1 = {}Text.up(State)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)end--? print('scroll up preserving cursor')--? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)end--? print('cursor is NOT at final screen line of its line')--? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))--? print('cursor pos is now', State.cursor1.line, State.cursor1.pos)--? print('scroll up preserving cursor')--? 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 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)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]endendendendendif Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State)endendwhile true dobreakendendendwhile true dobreakendText.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)breakendif Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') thenif 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)if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) thenendendfunction Text.skip_non_whitespace_rightA(State)while true dowhile true dobreakendbreakendendendfunction Text.skip_whitespace_rightB(State)Text.right_without_scroll(State)if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') thenif State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) thenfunction 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)Text.left(State)breakendbreakendendwhile 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') thenText.left(State)endendfunction Text.skip_non_whitespace_leftA(State)while true doif State.cursor1.posB == 1 thenbreakendif Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%S') thenbreakendendfunction Text.skip_whitespace_leftB(State)Text.left(State)if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') thenif State.cursor1.pos == 1 thenfunction 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)function 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) + 1endfunction Text.start_of_line(State)if Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1 = {line=State.cursor1.line, pos=1} -- copyif State.cursor1.pos thenState.cursor1.pos = 1elseState.cursor1.posB = 1end--? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)Text.snap_cursor_to_bottom_of_screen(State)State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1if scroll_down thenlocal 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)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)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]-- move down one screen line (A side) in current linelocal scroll_down = falselocal scroll_down = Text.le1(State.screen_bottom1, State.cursor1)elseif State.cursor1.pos thenText.snap_cursor_to_bottom_of_screen(State)endendendassert(false)endlocal screen_lineelseendlocal screen_line_text = App.newText(love.graphics.getFont(), screen_line)return App.width(screen_line_text)endreturn iendendfunction Text.screen_line_index(screen_line_starting_pos, pos)for i = #screen_line_starting_pos,1,-1 doif screen_line_starting_pos[i] <= pos thenfunction 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)elseendlocal screen_line_text = App.newText(love.graphics.getFont(), screen_line)return App.width(screen_line_text)end--? local result = App.width(screen_line_text)--? print('=>', result)--? return result--? print('final', i)--? print('after', start_offsetB)screen_line = string.sub(line.dataB, start_offsetB)--? print('between', start_offsetB, past_end_offsetB)screen_line = string.sub(line.dataB, start_offsetB, past_end_offsetB-1)endfunction 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 wrappingfunction Text.nearest_cursor_pos(line, x, left)if x < left then-- result: 1 to len+1screen_line = string.sub(line.data, start_pos)if 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)return 1endif x > max_x thenend--? print('-- nearest', x)while true doendif currxmin <= x and x < currxmax thenendendif currxmin > x thenelseendendendlocal len = utf8.len(line)if x > max_x thenreturn len+1endwhile true dolocal curr = math.floor((left+right)/2)if currxmin <= x and x < currxmax thenreturn currendif left >= right-1 thenreturn leftendif currxmin > x thenright = currelseleft = currendendassert(false)endresult.screen_line = ibreakendendassert(result.screen_pos)return resultendendreturn resultendfunction Text.lt1(a, b)if a.line < b.line thenreturn trueendif a.line > b.line thenreturn falseendreturn trueendreturn falseendendfunction 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 resultendelseendendif pos == State.screen_top1.pos thenbreakend-- make sure screen top is at start of a screen lineif State.screen_top1.pos - prev < pos - State.screen_top1.pos thenState.screen_top1.pos = prevelseendbreakendend-- make sure cursor is on screen--? print('too low')--? print('tweak')endendendendendendfunction Text.clear_screen_line_cache(State, line_index)State.line_cache[line_index].fragments = 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+$', '')State.line_cache[line_index].fragmentsB = nil--? print('clearing fragments')for i=1,#State.lines doState.line_cache[i] = {}State.line_cache = {}function Text.redraw_all(State)end-- slightly expensive since it redraws the screenApp.draw()-- this approach is cheaper and almost works, except on the final screen-- where file ends above bottom of screen--? local botline1 = {line=State.cursor1.line, pos=botpos}--? return Text.lt1(State.screen_bottom1, botline1)--? local botpos = Text.pos_at_start_of_screen_line(State, State.cursor1)return State.cursor_y == nilfunction Text.cursor_out_of_screen(State)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}if Text.cursor_out_of_screen(State) thenif 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 thenState.screen_top1.pos = poslocal prev = line_cache.screen_line_starting_pos[i-1]if pos > State.screen_top1.pos then-- resize helperif State.screen_top1.pos == 1 then return endlocal line = State.lines[State.screen_top1.line]for i=2,#line_cache.screen_line_starting_pos dolocal pos = line_cache.screen_line_starting_pos[i]local line_cache = State.line_cache[State.screen_top1.line]Text.populate_screen_line_starting_pos(State, State.screen_top1.line)function Text.tweak_screen_top_and_cursor(State)function Text.previous_screen_lineB(State, loc2)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}endendif 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}Text.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) thenreturn {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_posBif #screen_line_starting_posB > 1 thenreturn {line=loc2.line-1, screen_lineB=#State.line_cache[loc2.line-1].screen_line_starting_posB, screen_posB=1}else-- 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}end--? print('c3')--? print('c2')--? 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)--? print('c1', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, State.line_cache[loc2.line-1].fragmentsB)function Text.previous_screen_line(State, loc2)if loc2.screen_line > 1 then--? print('a')return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}elseif loc2.line == 1 then--? print('b')return loc2if 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 a.pos thenreturn a.pos < b.poselsereturn a.posB < b.posBendendfunction Text.le1(a, b)return eq(a, b) or Text.lt1(a, b)if not a.pos and b.pos then-- A side < B sideif a.pos and not b.pos thenendfunction 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 resultfunction 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 - 1return resultendfunction Text.to2B(State, loc1)local result = {line=loc1.line}if sposB <= loc1.posB thenresult.screen_lineB = iresult.screen_posB = loc1.posB - sposB + 1breakendendassert(result.screen_posB)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]result.screen_pos = loc1.pos - spos + 1Text.populate_screen_line_starting_pos(State, loc1.line)if spos <= loc1.pos thenfor i=#line_cache.screen_line_starting_pos,1,-1 dolocal spos = line_cache.screen_line_starting_pos[i]function 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)local result = {line=loc1.line}local line_cache = State.line_cache[loc1.line]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]endlocal 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)function Text.x_after(s, pos)--? print('', x, left, right, curr, currxmin, currxmax)local currxmin = Text.x_after(line, curr+1)local currxmax = Text.x_after(line, curr+2)local left, right = 0, len+1local max_x = Text.x_after(line, len)-- return the nearest index of line (in utf8 code points) which lies entirely-- within x pixels of the left marginfunction Text.nearest_pos_less_than(line, x)--? print('', '-- nearest_pos_less_than', line, x)-- result: 0 to len+1assert(false)leftpos = currrightpos = currif leftpos >= rightpos-1 thenreturn rightposif x-currxmin < currxmax-x thenreturn currelsereturn curr+1end--? print('nearest', x, leftpos, rightpos, curr, currxmin, currxmax)local curr = math.floor((leftpos+rightpos)/2)local currxmin = left+Text.x(line, curr)local currxmax = left+Text.x(line, curr+1)--? print('nearest', x, '^'..line..'$', leftpos, rightpos)if leftpos == rightpos thenreturn leftposlocal leftpos, rightpos = 1, len+1return len+1local len = utf8.len(line)local max_x = left+Text.x(line, len+1)function Text.screen_line_width(State, line_index, i)local line = State.lines[line_index]local start_pos = line_cache.screen_line_starting_pos[i]local start_offset = Text.offset(line.data, start_pos)local line_cache = State.line_cache[line_index]end--? print(screen_lines[#screen_lines], State.cursor1.pos)if (not State.expanded and not line.expanded) orreturn 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.posBline.dataB == nil thenend--? print('scroll up')endendwhile true doendend--? 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)Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaksFoo = true--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)endif 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) + 1endText.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 sideendlocal x = Margin_left + Text.screen_line_width(State, line_index, #line_cache.screen_line_starting_pos) + AB_padding--? print('#screenlines after A', num_screen_lines)if line.dataB and (State.expanded or line.expanded) thenend--? print('#screenlines after B', num_screen_lines)return y < line_cache.starty + State.line_height*num_screen_linesendassert(my >= line_cache.starty)-- duplicate some logic from Text.drawlocal 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('past end of non-final line; return')return nil, line_cache.screen_line_starting_posB[screen_line_indexB+1]-1endendy = nextyendassert(false)local s = string.sub(line.dataB, screen_line_starting_byte_offsetB)--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1)return nil, screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1if 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('aa', mx, State.left, Text.screen_line_widthB(State, line_index, screen_line_indexB))local y = line_cache.startyif 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)-- no B sideendlocal 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 sidefor 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))assert(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 aboveendmx = mx - max_xA - AB_paddingreturn 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 - 1if line.dataB == nil thenlocal screen_line_posA = Text.nearest_cursor_pos(s, mx, State.left)--? print('click', line_index, my, 'with line starting at', y, #line_cache.screen_line_starting_pos) -- , #line_cache.screen_line_starting_posB)-- convert mx,my in pixels to schema-1 coordinates-- returns: pos, posBfunction Text.to_pos_on_line(State, line_index, mx, my)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]-- 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.in_line(State, line_index, x,y)local line = State.lines[line_index]if line_cache.starty == nil then return false end -- outside current pagelocal line_cache = State.line_cache[line_index]y = y - htop2 = Text.previous_screen_line(State, top2)if top2.line == 1 and top2.screen_line == 1 then break endlocal h = State.line_heightif y - h < State.top thenbreak--? print(y, 'top2:', State.lines[top2.line].data, top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB)-- duplicate some logic from love.draw--? 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-- should never modify State.cursor1function Text.snap_cursor_to_bottom_of_screen(State)local top2 = Text.to2(State, State.cursor1)--? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos)-- slide to start of screen line--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)top2.screen_pos = 1 -- start of screen line--? 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('to2:', State.cursor1.line, State.cursor1.pos)--? print('to2:', State.cursor1.line, State.cursor1.pos, State.cursor1.posB)Text.snap_cursor_to_bottom_of_screen(State)function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)if State.top > App.screen.height - State.line_height thenfunction 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_posreturn spos,iendendassert(false)endreturn sposB,ifunction Text.pos_at_start_of_screen_lineB(State, loc1)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 thenText.populate_screen_line_starting_pos(State, loc1.line)local line_cache = State.line_cache[loc1.line]function Text.pos_at_start_of_screen_line(State, loc1)Text.populate_screen_line_starting_pos(State, loc1.line)local line_cache = State.line_cache[loc1.line]for i=#line_cache.screen_line_starting_pos,1,-1 dolocal spos = line_cache.screen_line_starting_pos[i]if spos <= loc1.pos thenendendfunction 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)endendendendText.right_without_scroll(State)Text.snap_cursor_to_bottom_of_screen(State)if Text.cursor_out_of_screen(State) thenendendendfunction Text.right_without_scroll(State)State.cursor1.pos = State.cursor1.pos+1elseif State.cursor1.line <= #State.lines-1 then-- overflow back into A sideState.cursor1 = {line=State.cursor1.line+1, pos=1}elseif State.cursor1.line <= #State.lines-1 thenState.cursor1 = {line=State.cursor1.line+1, pos=1}endendfunction Text.right_without_scrollB(State)if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) thenState.cursor1.posB = State.cursor1.posB+1if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenif State.cursor1.pos thenText.right_without_scrollA(State)elseText.right_without_scrollB(State)endendfunction Text.right_without_scrollA(State)function Text.right(State)if Text.lt1(State.cursor1, State.screen_top1) thenState.screen_top1 = Text.to1(State, top2)local top2 = Text.to2(State, State.screen_top1)top2 = Text.previous_screen_line(State, top2)if State.cursor1.pos > 1 thenState.cursor1.pos = State.cursor1.pos-1elseif State.cursor1.line > 1 thenState.cursor1 = {line = State.cursor1.line-1,}endif Text.lt1(State.cursor1, State.screen_top1) thenlocal top2 = Text.to2(State, State.screen_top1)top2 = Text.previous_screen_line(State, top2)State.screen_top1 = Text.to1(State, top2)endendfunction Text.leftB(State)if State.cursor1.posB > 1 thenState.cursor1.posB = State.cursor1.posB-1else-- overflow back into A sideState.cursor1.posB = nilState.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1pos = utf8.len(State.lines[State.cursor1.line-1].data) + 1,function Text.left(State)if State.cursor1.pos thenText.leftA(State)elseText.leftB(State)endendfunction Text.leftA(State)if State.cursor1.line > State.screen_bottom1.line then--? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)Text.down(State)State.selection1 = {}Text.up(State)State.selection1 = {}Text.end_of_line(State)Text.start_of_line(State)Text.end_of_line(State)State.selection1 = {}Text.start_of_line(State)Text.word_right(State)State.selection1 = {}Text.word_left(State)State.selection1 = {}Text.right(State)State.selection1 = {}Text.left(State)State.selection1 = {}schedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})Text.clear_screen_line_cache(State, State.cursor1.line)table.remove(State.line_cache, State.cursor1.line+1)elseif State.cursor1.line < #State.lines then-- join linesState.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data-- delete side B on first lineState.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataBelseif 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 Bend-- no change to State.cursor1.posState.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)local beforeelsebefore = snapshot(State, State.cursor1.line, State.cursor1.line+1)before = snapshot(State, State.cursor1.line)if State.cursor1.posB or State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenassert(Text.le1(State.screen_top1, State.cursor1))schedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})Text.clear_screen_line_cache(State, State.cursor1.line)State.screen_top1 = Text.to1(State, top2)Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leakslocal top2 = Text.to2(State, State.screen_top1)if 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) thenelseif State.cursor1.line > 1 thenbefore = snapshot(State, State.cursor1.line-1, State.cursor1.line)-- join linestable.remove(State.lines, State.cursor1.line)table.remove(State.line_cache, State.cursor1.line)State.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].dataState.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 BState.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)local beforebefore = 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 State.cursor1.pos and State.cursor1.pos > 1 thenschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})Text.insert_at_cursor(State, '\t')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)if State.cursor_y > App.screen.height - State.line_height thenText.populate_screen_line_starting_pos(State, State.cursor1.line)local before = snapshot(State, State.cursor1.line)local before_line = State.cursor1.linelocal before = snapshot(State, before_line)--== shortcuts that mutate textfunction Text.keychord_pressed(State, chord)--? print('chord', chord)function 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+1endText.insert_at_cursor(State, t)Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)if State.cursor_y > App.screen.height - State.line_height thenlocal before = snapshot(State, State.cursor1.line)Text.populate_screen_line_starting_pos(State, State.cursor1.line)function Text.textinput(State, t)--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')table.insert(line_cache.fragmentsB, {data=frag, text=frag_text})x = State.left -- new linelocal frag_text = App.newText(love.graphics.getFont(), frag)local frag_width = App.width(frag_text)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))--? 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)local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos--? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')local frag1 = string.sub(frag, 1, boffset-1)local frag1_text = App.newText(love.graphics.getFont(), frag1)local frag1_width = App.width(frag1_text)--? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')assert(x + frag1_width <= State.right)frag = string.sub(frag, boffset)frag_text = App.newText(love.graphics.getFont(), frag)frag_width = App.width(frag_text)table.insert(line_cache.fragmentsB, {data=frag1, text=frag1_text})if bpos == 0 then break end -- avoid infinite loop when window is too narrowif (x-State.left) < 0.8 * (State.right-State.left) then--? print('x: '..tostring(x)..'; frag_width: '..tostring(frag_width)..'; '..tostring(State.right-x)..'px to go')for frag in line.data:gmatch('%S*%s*') dolocal frag_text = App.newText(love.graphics.getFont(), frag)local frag_width = App.width(frag_text)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)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')endx = x + frag_widthendendlocal line = State.lines[line_index]local line_cache = State.line_cache[line_index]returnend-- duplicate some logic from Text.drawlocal pos = 1local frag, frag_text = f.data, f.text-- render fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenx = State.leftendx = x + frag_widthlocal frag_len = utf8.len(frag)pos = pos + frag_lenendend-- try to wrap at word boundariesfor frag in line.dataB:gmatch('%S*%s*') do--? print('compute_fragmentsB', line_index, 'between', x, State.right)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]line_cache.fragmentsB = {}if line_cache.fragmentsB thenreturnendfunction Text.compute_fragmentsB(State, line_index, x)table.insert(line_cache.screen_line_starting_posB, pos)for _, f in ipairs(line_cache.fragmentsB) doline_cache.screen_line_starting_posB = {1}Text.compute_fragmentsB(State, line_index, x)if line_cache.screen_line_starting_posB thenfunction Text.populate_screen_line_starting_posB(State, line_index, x)table.insert(line_cache.fragments, {data=frag, text=frag_text})table.insert(line_cache.fragments, {data=frag1, text=frag1_text})--? print('x: '..tostring(x)..'; frag_width: '..tostring(frag_width)..'; '..tostring(State.right-x)..'px to go')--? print('compute_fragments', line_index, 'between', State.left, State.right)if line_cache.fragments thenreturnendline_cache.fragments = {}line_cache.fragments = {}local line = State.lines[line_index]local line_cache = State.line_cache[line_index]function Text.compute_fragments(State, line_index)x = x + frag_widthApp.screen.draw(frag_text, x,y)-- render cursor if necessaryif State.search_term thenlove.graphics.print(State.search_term, x+lo_px,y)endText.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y)App.color(Fold_color)elseif Focus == 'edit' thenApp.color(Fold_color)if 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))if State.cursor1.posB and line_index == State.cursor1.line thenif pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB thenx = State.leftreturn --[[screen filled]] true, x,y, pos, screen_line_starting_poslocal frag_len = utf8.len(frag)-- render nothing--? print('skipping', frag)elseif x + frag_width > State.right thenassert(x > State.left) -- no overfull lines-- render fragmentlocal frag_width = App.width(frag_text)--? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)if pos < startpos thenText.compute_fragments(State, line_index)local pos = 1initialize_color()return y, nil, screen_line_starting_posend-- check for B sideassert(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)App.color(Fold_background_color)end,onpress1 = function()line.expanded = trueend,})return y, screen_line_starting_posend-- draw B sideApp.color(Fold_color)if overflows_screen thenend--? if line_index == 8 then print('a') end--? if line_index == 8 then print('b') endif State.search_term == nil thenif line_index == State.cursor1.line and State.cursor1.posB == pos thenText.draw_cursor(State, x, y)--? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) endif Focus == 'edit' and State.cursor1.posB thenreturn y, nil, screen_line_starting_posif 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)end--? if Foo then--? print('draw:', State.lines[line_index].data, "=====", State.lines[line_index].dataB, 'starting from x', x+AB_padding)--? end--? if line_index == 8 then print('drawing B side') endlove.graphics.rectangle('fill', button_params.x, button_params.y, App.width(State.em), State.line_height-4, 2,2)--? 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)--? if line_index == 8 then print('checking for B side') endif line.dataB == nil thenlocal line = State.lines[line_index]line_cache.starty = yline_cache.startpos = startpos-- draw A sideendendelsex = State.leftlocal 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 State.search_term == nil thenif line_index == State.cursor1.line and State.cursor1.pos == pos thenText.draw_cursor(State, x, y)endif Focus == 'edit' and State.cursor1.pos thenline_cache.startposB = startposBlocal line_cache = State.line_cache[line_index]-- draw a line starting from startpos to screen at y between State.left and State.right-- return the final y, and pos,posB of start of final screen line drawnfunction Text.draw(State, line_index, y, startpos, startposB)AB_padding = 20 -- space in pixels between A side and B side-- text editor, particularly text drawing, horizontal wrap, vertical scrolling
-- primitives for saving to file and loading from fileif 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 endendendif #result == 0 thenendreturn resultendif outfile == nil thenendendoutfile:close()endend-- for testsfunction load_array(a)local result = {}local next_line = ipairs(a)while true doi,line = next_line(a, i)if i == nil then break endendif #result == 0 thenendreturn resulttable.insert(result, {data=''})local line_info = {}elseline_info.data = lineendtable.insert(result, line_info)if line:find(Fold) then_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')local i,line,drawing = 0, ''for _,line in ipairs(State.lines) dooutfile:write(line.data)outfile:write('\n')outfile:write(line.data)outfile:write(Fold)endoutfile:write('\n')outfile:write(line.dataB)if line.dataB and #line.dataB > 0 thenerror('failed to write to "'..State.filename..'"')function save_to_disk(State)local outfile = App.open_for_writing(State.filename)table.insert(result, {data=''})local line_info = {}table.insert(result, line_info)elseline_info.data = lineendif line:find(Fold) then_, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)')function load_from_disk(State)local infile = App.open_for_reading(State.filename)State.lines = load_from_file(infile)function file_exists(filename)local infile = App.open_for_reading(filename)if infile theninfile:close()return trueelsereturn falseendendFold = '\x1e' -- ASCII RS (record separator)
-- 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 textMargin_top = 15Margin_left = 25Margin_right = 25edit = {}-- run in both tests and a real runlocal result = {-- a line of bifold text consists of an A side and an optional B side, each of which is a string-- expanded: whether to show B sidelines = {{data='', dataB=nil, expanded=nil}}, -- array of linesfont_height = font_height,line_height = line_height,em = App.newText(love.graphics.getFont(), 'm'), -- widest possible character widthfilename = love.filesystem.getUserDirectory()..'/lines.txt',next_save = nil,--? print('== draw')--? print('draw:', y, line_index, line)endy = y + State.line_height--? print('=> y', y)endendendif State.next_save and State.next_save < App.getTime() thenState.next_save = nilendendendend-- make sure to save before quittingendendendendendendelseendendif chord == 'escape' thenelseif chord == 'return' thenelseif chord == 'backspace' thenelseif chord == 'down' thenelseif chord == 'up' thenendreturnelseif chord == 'C-f' thenelseif chord == 'C-=' thenelseif chord == 'C--' thenelseif chord == 'C-0' thenelseif chord == 'C-z' thenif event thenlocal src = event.beforeschedule_save(State)endelseif chord == 'C-y' thenif event thenlocal src = event.afterschedule_save(State)end-- clipboardelseif chord == 'C-c' thenif s thenApp.setClipboardText(s)endelseif chord == 'C-x' thenlocal s = Text.cut_selection(State, State.left, State.right)if s thenApp.setClipboardText(s)endelseif chord == 'C-v' then-- 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 clipboard_data = App.getClipboardText()for _,code in utf8.codes(clipboard_data) dolocal c = utf8.char(code)if c == '\n' thenelseendendText.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endelseendendend-- 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)endApp.screen.contents = {}edit.draw(State)endApp.screen.contents = {}edit.draw(State)endApp.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)function 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)function 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)function edit.key_released(State, key, scancode)end-- 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)--== some methods for testsTest_margin_left = 25function edit.initialize_test_state()-- if you change these values, tests will start failingreturn edit.initialize_state(15, -- top marginTest_margin_left,App.screen.width, -- right margin = 014, -- font height assuming default LÖVE font15) -- line heightendfunction 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')endText_cache = {}Text.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)for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})-- dispatch to textif Text.cursor_out_of_screen(State) thenText.insert_at_cursor(State, c)Text.insert_return(State)local before_line = State.cursor1.linelocal before = snapshot(State, before_line)for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollschedule_save(State)for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrolllocal s = Text.selection(State)State.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)local event = redo_event(State)for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollState.screen_top1 = deepcopy(src.screen_top)State.cursor1 = deepcopy(src.cursor)patch(State.lines, event.after, event.before)-- if we're scrolling, reclaim all fragments to avoid memory leaksText.redraw_all(State)patch_placeholders(State.line_cache, event.after, event.before)local event = undo_event(State)for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollText.redraw_all(State)-- undoedit.update_font_settings(State, 20)Text.redraw_all(State)edit.update_font_settings(State, State.font_height-2)Text.redraw_all(State)edit.update_font_settings(State, State.font_height+2)State.search_term = ''assert(State.search_text == nil)-- bifold textelseif chord == 'C-b' thenState.expanded = not State.expandedText.redraw_all(State)if not State.expanded thenfor _,line in ipairs(State.lines) doline.expanded = nilendendelseif chord == 'C-d' thenif State.cursor1.posB == nil thenif 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)endendschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})local before = snapshot(State, State.cursor1.line)edit.eradicate_locations_after_the_fold(State)-- zoomState.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, posB=State.cursor1.posB},screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB},Text.search_previous(State)Text.search_next(State)if State.cursor1.pos thenState.cursor1.pos = State.cursor1.pos+1elseState.cursor1.posB = State.cursor1.posB+1endlocal 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 = nilState.search_term = nilState.search_text = nilState.search_backup = nilState.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 leaksif State.search_term thenfunction edit.keychord_pressed(State, chord, key)schedule_save(State)Text.textinput(State, t)function edit.textinput(State, t)if State.search_term thenState.search_term = State.search_term..tState.search_text = nilText.search_next(State)for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scrollfunction edit.mouse_released(State, x,y, mouse_button)for line_index,line in ipairs(State.lines) doif Text.in_line(State, line_index, x,y) thenbreaklocal pos,posB = Text.to_pos_on_line(State, line_index, x, y)--? print(x,y, 'setting cursor:', line_index, pos, posB)State.cursor1 = {line=line_index, pos=pos, posB=posB}function edit.mouse_pressed(State, x,y, mouse_button)if State.search_term then return end-- press on a button and it returned 'true' to short-circuitreturnendif mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then--? print('press', State.selection1.line, State.selection1.pos)if State.next_save thensave_to_disk(State)function edit.quit(State)function 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 didsave_to_disk(State)function edit.update(State, dt)if State.search_term thenText.draw_search_bar(State)endy, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB)if y + State.line_height > App.screen.height then break end--? print('text.draw', y, line_index)if line_index == State.screen_top1.line thenstartpos = State.screen_top1.posif State.screen_top1.pos thenstartpos = State.screen_top1.poselsestartpos, startposB = nil, State.screen_top1.posBendlocal startpos = 1local startpos, startposB = 1, nilState.screen_bottom1 = {line=line_index, pos=nil}State.screen_bottom1 = {line=line_index, pos=nil, posB=nil}for line_index = State.screen_top1.line,#State.lines dolocal line = State.lines[line_index]App.color(Text_color)assert(#State.lines == #State.line_cache)local y = State.topif not Text.le1(State.screen_top1, State.cursor1) thenassert(false)endState.cursor_x = nilState.cursor_y = nilprint(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB)function edit.draw(State)State.button_handlers = {}-- searchsearch_term = nil,search_text = nil,search_backup = nil, -- stuff to restore when cancelling search}return resultend -- App.initialize_state-- undohistory = {},next_history = 1,top = top,left = left,right = right,width = right-left,-- cursor coordinates in pixelscursor_x = 0,cursor_y = 0,-- Lines can be too long to fit on screen, in which case they _wrap_ into-- multiple _screen lines_.-- * 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.---- 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-- Positions (and screen line indexes) can be in either the A or the B side.-- rendering wrapped text lines needs some additional short-lived data per line:-- startpos, the index of data the line starts rendering from, can only be >1 for topmost line on screen-- starty, the y coord in pixels the line starts rendering from-- fragments: snippets of rendered love.graphics.Text, guaranteed to not straddle screen lines-- screen_line_starting_pos: optional array of grapheme indices if it wraps over more than one screen line-- Given wrapping, any potential location for the text cursor can be described in two ways:line_cache = {},function edit.initialize_state(top, left, right, font_height, line_height) -- currently always draws to bottom of screenFold_color = {r=0, g=0.6, b=0}Fold_background_color = {r=0, g=0.7, b=0}
if x < Editor_state.right + Margin_right then--? print('click on edit side')if Focus ~= 'edit' thenFocus = 'edit'endedit.mouse_pressed(Editor_state, x,y, mouse_button)elseif Show_log_browser_side and Log_browser_state.left <= x and x < Log_browser_state.right then--? print('click on log_browser side')if Focus ~= 'log_browser' thenFocus = 'log_browser'endlog_browser.mouse_pressed(Log_browser_state, x,y, mouse_button)for _,line_cache in ipairs(Editor_state.line_cache) do line_cache.starty = nil end -- just in case we scrollendendfunction source.mouse_released(x,y, mouse_button)Cursor_time = 0 -- ensure cursor is visible immediately after it movesif Focus == 'edit' thenreturn edit.mouse_released(Editor_state, x,y, mouse_button)elsereturn log_browser.mouse_released(Log_browser_state, x,y, mouse_button)endendfunction source.textinput(t)Cursor_time = 0 -- ensure cursor is visible immediately after it movesif Focus == 'edit' thenreturn edit.textinput(Editor_state, t)elsereturn log_browser.textinput(Log_browser_state, t)endendfunction source.keychord_pressed(chord, key)Cursor_time = 0 -- ensure cursor is visible immediately after it moves--? print('source keychord')if Show_file_navigator thenkeychord_pressed_on_file_navigator(chord, key)returnendif chord == 'C-l' then--? print('C-l')Show_log_browser_side = not Show_log_browser_sideif Show_log_browser_side thenApp.screen.width = Log_browser_state.right + Margin_rightelseApp.screen.width = Editor_state.right + Margin_rightend--? print('setting window:', App.screen.width, App.screen.height)love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)--? print('done setting window')-- try to restore position if possible-- if the window gets wider the window manager may not respect thissource.set_window_position_from_settings(Settings.source)returnendif chord == 'C-g' thenShow_file_navigator = trueFile_navigation.index = 1returnendif Focus == 'edit' thenreturn edit.keychord_pressed(Editor_state, chord, key)elsereturn log_browser.keychord_pressed(Log_browser_state, chord, key)endendfunction source.key_released(key, scancode)Cursor_time = 0 -- ensure cursor is visible immediately after it movesif Focus == 'edit' thenreturn edit.key_released(Editor_state, key, scancode)elsereturn log_browser.keychord_pressed(Log_browser_state, chordkey, scancode)endend-- use this sparinglyfunction to_text(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endreturn Text_cache[s]end'geom','drawing_tests','file','source','source_tests','commands','log_browser','source_edit','source_text','source_undo','colorize','source_text_tests','source_file','main','button','keychord','app','test','json',},index = 1,}Menu_status_bar_height = nil -- initialized below-- a few text objects we can avoid recomputing unless the font changesText_cache = {}-- blinking cursorCursor_time = 0end-- called only for real runfunction source.initialize()love.keyboard.setTextInput(true) -- bring up keyboard on touch screenlove.keyboard.setKeyRepeat(true)love.graphics.setBackgroundColor(1,1,1)if Settings and Settings.source thensource.load_settings()elsesource.initialize_default_settings()endsource.initialize_edit_side{'run.lua'}source.initialize_log_browser_side()Menu_status_bar_height = 5 + Editor_state.line_height + 5Editor_state.top = Editor_state.top + Menu_status_bar_heightLog_browser_state.top = Log_browser_state.top + Menu_status_bar_height'drawing','help','text','search','select','undo','text_tests',
add_hotkey_to_menu('ctrl+i: create/edit debug print')add_hotkey_to_menu('ctrl+f: find in file')add_hotkey_to_menu('alt+left alt+right: prev/next word')elseif Focus == 'log_browser' then-- nothing yetelseassert(false, 'unknown focus "'..Focus..'"')endadd_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')endfunction add_hotkey_to_menu(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endlocal width = App.width(Text_cache[s])if Menu_cursor + width > App.screen.width - 5 thenreturnendApp.color(Menu_command_color)App.screen.draw(Text_cache[s], Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction source.draw_file_navigator()for i,file in ipairs(File_navigation.candidates) doif file == 'source' thenApp.color(Menu_border_color)love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)endadd_file_to_menu(file, i == File_navigation.index)endendfunction add_file_to_menu(s, cursor_highlight)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endlocal width = App.width(Text_cache[s])if Menu_cursor + width > App.screen.width - 5 thenreturnendif cursor_highlight thenApp.color(Menu_highlight_color)love.graphics.rectangle('fill', Menu_cursor-5,5-2, App.width(Text_cache[s])+5*2,Editor_state.line_height+2*2)endApp.color(Menu_command_color)App.screen.draw(Text_cache[s], Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction keychord_pressed_on_file_navigator(chord, key)if chord == 'escape' thenShow_file_navigator = falseelseif chord == 'return' thenlocal candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')source.switch_to_file(candidate)Show_file_navigator = falseelseif chord == 'left' thenif File_navigation.index > 1 thenFile_navigation.index = File_navigation.index-1endelseif chord == 'right' thenif File_navigation.index < #File_navigation.candidates thenFile_navigation.index = File_navigation.index+1endendend
current_drawing_mode=Drawing_mode,previous_drawing_mode=State.previous_drawing_mode,if 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)end
-- 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),
endreturn eventendfunction patch(lines, from, to)--? if #from.lines == 1 and #to.lines == 1 then--? assert(from.start_line == from.end_line)--? assert(to.start_line == to.end_line)--? assert(from.start_line == to.start_line)--? lines[from.start_line] = to.lines[1]--? return--? endassert(from.start_line == to.start_line)for i=from.end_line,from.start_line,-1 dotable.remove(lines, i)endassert(#to.lines == to.end_line-to.start_line+1)for i=1,#to.lines dotable.insert(lines, to.start_line+i-1, to.lines[i])endendfunction patch_placeholders(line_cache, from, to)assert(from.start_line == to.start_line)for i=from.end_line,from.start_line,-1 dotable.remove(line_cache, i)endassert(#to.lines == to.end_line-to.start_line+1)for i=1,#to.lines dotable.insert(line_cache, to.start_line+i-1, {})endend-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080function deepcopy(obj, seen)if type(obj) ~= 'table' then return obj endif seen and seen[obj] then return seen[obj] endlocal s = seen or {}local result = setmetatable({}, getmetatable(obj))s[obj] = resultfor k,v in pairs(obj) doresult[deepcopy(k, s)] = deepcopy(v, s)endreturn resultendfunction minmax(a, b)return math.min(a,b), math.max(a,b)end
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')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')Editor_state.lines = load_array{'```lines', '```', 'def', 'ghi', 'deg'}
-- 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')
function test_move_to_start_of_previous_word()io.write('\ntest_move_to_start_of_previous_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_to_start_of_previous_word')endfunction test_skip_to_previous_word()io.write('\ntest_skip_to_previous_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=5} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'F - test_skip_to_previous_word')endfunction test_skip_past_tab_to_previous_word()io.write('\ntest_skip_past_tab_to_previous_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def\tghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=10} -- within third wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 9, 'F - test_skip_past_tab_to_previous_word')endfunction test_skip_multiple_spaces_to_previous_word()io.write('\ntest_skip_multiple_spaces_to_previous_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=6} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.pos, 1, 'F - test_skip_multiple_spaces_to_previous_word')endfunction test_move_to_start_of_word_on_previous_line()io.write('\ntest_move_to_start_of_word_on_previous_line')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-left')check_eq(Editor_state.cursor1.line, 1, 'F - test_move_to_start_of_word_on_previous_line/line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_move_to_start_of_word_on_previous_line/pos')endfunction test_move_past_end_of_word()io.write('\ntest_move_past_end_of_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 4, 'F - test_move_past_end_of_word')endfunction test_skip_to_next_word()io.write('\ntest_skip_to_next_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 8, 'F - test_skip_to_next_word')endfunction test_skip_past_tab_to_next_word()io.write('\ntest_skip_past_tab_to_next_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc\tdef'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1} -- at the space between wordsedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 4, 'F - test_skip_past_tab_to_next_word')endfunction test_skip_multiple_spaces_to_next_word()io.write('\ntest_skip_multiple_spaces_to_next_word')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=4} -- at the start of second wordedit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.pos, 9, 'F - test_skip_multiple_spaces_to_next_word')endfunction test_move_past_end_of_word_on_next_line()io.write('\ntest_move_past_end_of_word_on_next_line')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=8}edit.draw(Editor_state)edit.run_after_keychord(Editor_state, 'M-right')check_eq(Editor_state.cursor1.line, 2, 'F - test_move_past_end_of_word_on_next_line/line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_move_past_end_of_word_on_next_line/pos')endfunction test_click_with_mouse()io.write('\ntest_click_with_mouse')-- display two lines with cursor on one of themApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse/cursor:line')endfunction test_click_with_mouse_to_left_of_line()io.write('\ntest_click_with_mouse_to_left_of_line')-- display a line with the cursor in the middleApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=3}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click to the left of the lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left-4,Editor_state.top+5, 1)-- cursor moves to start of linecheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_to_left_of_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_click_with_mouse_to_left_of_line/cursor:pos')endfunction test_click_with_mouse_takes_margins_into_account()io.write('\ntest_click_with_mouse_takes_margins_into_account')-- display two lines with cursor on one of themApp.screen.init{width=100, height=80}Editor_state = edit.initialize_test_state()Editor_state.left = 50 -- occupy only right side of screenEditor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_takes_margins_into_account/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_takes_margins_into_account/cursor:pos')endfunction test_click_with_mouse_on_empty_line()io.write('\ntest_click_with_mouse_on_empty_line')-- display two lines with the first one emptyApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the empty lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_empty_line/cursor')endfunction test_draw_text()io.write('\ntest_draw_text')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_draw_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_draw_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_draw_text/screen:3')endfunction test_draw_wrapping_text()io.write('\ntest_draw_wrapping_text')App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'defgh', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_draw_wrapping_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'de', 'F - test_draw_wrapping_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'fgh', 'F - test_draw_wrapping_text/screen:3')endfunction test_draw_word_wrapping_text()io.write('\ntest_draw_word_wrapping_text')App.screen.init{width=60, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc ', 'F - test_draw_word_wrapping_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def ', 'F - test_draw_word_wrapping_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_draw_word_wrapping_text/screen:3')endfunction test_click_with_mouse_on_wrapping_line()io.write('\ntest_click_with_mouse_on_wrapping_line')-- display two lines with cursor on one of themApp.screen.init{width=50, height=80}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi jkl mno pqr stu'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=20}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line/cursor:pos')endfunction test_click_with_mouse_on_wrapping_line_takes_margins_into_account()io.write('\ntest_click_with_mouse_on_wrapping_line_takes_margins_into_account')-- display two lines with cursor on one of themApp.screen.init{width=100, height=80}Editor_state = edit.initialize_test_state()Editor_state.left = 50 -- occupy only right side of screenEditor_state.lines = load_array{'abc def ghi jkl mno pqr stu'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=20}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- click on the other lineedit.draw(Editor_state)edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)-- cursor movescheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:pos')endfunction test_draw_text_wrapping_within_word()-- arrange a screen line that needs to be split within a wordio.write('\ntest_draw_text_wrapping_within_word')App.screen.init{width=60, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abcd e fghijk', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abcd ', 'F - test_draw_text_wrapping_within_word/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'e fgh', 'F - test_draw_text_wrapping_within_word/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ijk', 'F - test_draw_text_wrapping_within_word/screen:3')endfunction test_draw_wrapping_text_containing_non_ascii()-- draw a long line containing non-ASCIIio.write('\ntest_draw_wrapping_text_containing_non_ascii')App.screen.init{width=60, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'madam I’m adam', 'xyz'} -- notice the non-ASCII apostropheText.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'mad', 'F - test_draw_wrapping_text_containing_non_ascii/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'am I', 'F - test_draw_wrapping_text_containing_non_ascii/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, '’m a', 'F - test_draw_wrapping_text_containing_non_ascii/screen:3')endfunction test_click_on_wrapping_line()io.write('\ntest_click_on_wrapping_line')-- display a wrapping lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'F - test_click_on_wrapping_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, "I'm ad", 'F - test_click_on_wrapping_line/baseline/screen:2')y = y + Editor_state.line_height-- click past end of second screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_on_wrapping_line/cursor:line')check_eq(Editor_state.cursor1.pos, 12, 'F - test_click_on_wrapping_line/cursor:pos')endfunction test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen()io.write('\ntest_click_on_wrapping_line_rendered_from_partway_at_top_of_screen')-- display a wrapping line from its second screen lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=8}Editor_state.screen_top1 = {line=1, pos=7}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, "I'm ad", 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/baseline/screen:2')y = y + Editor_state.line_height-- click past end of second screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.line, 1, 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/cursor:line')check_eq(Editor_state.cursor1.pos, 12, 'F - test_click_on_wrapping_line_rendered_from_partway_at_top_of_screen/cursor:pos')endfunction test_click_past_end_of_wrapping_line()io.write('\ntest_click_past_end_of_wrapping_line')-- display a wrapping lineApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{"madam I'm adam"}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, "I'm ad", 'F - test_click_past_end_of_wrapping_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'am', 'F - test_click_past_end_of_wrapping_line/baseline/screen:3')y = y + Editor_state.line_height-- click past the end of itedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of linecheck_eq(Editor_state.cursor1.pos, 15, 'F - test_click_past_end_of_wrapping_line/cursor') -- one more than the number of UTF-8 code-pointsendfunction test_click_past_end_of_wrapping_line_containing_non_ascii()io.write('\ntest_click_past_end_of_wrapping_line_containing_non_ascii')-- display a wrapping line containing non-ASCIIApp.screen.init{width=75, height=80}Editor_state = edit.initialize_test_state()-- 12345678901234Editor_state.lines = load_array{'madam I’m adam'} -- notice the non-ASCII apostropheText.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'I’m ad', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'am', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:3')y = y + Editor_state.line_height-- click past the end of itedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of linecheck_eq(Editor_state.cursor1.pos, 15, 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/cursor') -- one more than the number of UTF-8 code-pointsendfunction test_click_past_end_of_word_wrapping_line()io.write('\ntest_click_past_end_of_word_wrapping_line')-- display a long line wrapping at a word boundary on a screen of more realistic lengthApp.screen.init{width=160, height=80}Editor_state = edit.initialize_test_state()-- 0 1 2-- 123456789012345678901Editor_state.lines = load_array{'the quick brown fox jumped over the lazy dog'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'the quick brown fox ', 'F - test_click_past_end_of_word_wrapping_line/baseline/screen:1')y = y + Editor_state.line_height-- click past the end of the screen lineedit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)-- cursor moves to end of screen linecheck_eq(Editor_state.cursor1.pos, 20, 'F - test_click_past_end_of_word_wrapping_line/cursor')endfunction test_edit_wrapping_text()io.write('\ntest_edit_wrapping_text')App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=4}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)edit.run_after_textinput(Editor_state, 'g')local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_edit_wrapping_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'de', 'F - test_edit_wrapping_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'fg', 'F - test_edit_wrapping_text/screen:3')endfunction test_insert_newline()io.write('\ntest_insert_newline')-- display a few linesApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_insert_newline/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_insert_newline/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_insert_newline/baseline/screen:3')-- hitting the enter key splits the lineedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.screen_top1.line, 1, 'F - test_insert_newline/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline/cursor:pos')y = Editor_state.topApp.screen.check(y, 'a', 'F - test_insert_newline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'bc', 'F - test_insert_newline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_insert_newline/screen:3')endfunction test_insert_newline_at_start_of_line()io.write('\ntest_insert_newline_at_start_of_line')-- display a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- hitting the enter key splits the lineedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_newline_at_start_of_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_insert_newline_at_start_of_line/cursor:pos')check_eq(Editor_state.lines[1].data, '', 'F - test_insert_newline_at_start_of_line/data:1')check_eq(Editor_state.lines[2].data, 'abc', 'F - test_insert_newline_at_start_of_line/data:2')endfunction test_insert_from_clipboard()io.write('\ntest_insert_from_clipboard')-- display a few linesApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_insert_from_clipboard/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_insert_from_clipboard/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_insert_from_clipboard/baseline/screen:3')-- paste some text including a newline, check that new line is createdApp.clipboard = 'xy\nz'edit.run_after_keychord(Editor_state, 'C-v')check_eq(Editor_state.screen_top1.line, 1, 'F - test_insert_from_clipboard/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_insert_from_clipboard/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_insert_from_clipboard/cursor:pos')y = Editor_state.topApp.screen.check(y, 'axy', 'F - test_insert_from_clipboard/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'zbc', 'F - test_insert_from_clipboard/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_insert_from_clipboard/screen:3')endfunction test_move_cursor_using_mouse()io.write('\ntest_move_cursor_using_mouse')App.screen.init{width=50, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state) -- populate line_cache.starty for each line Editor_state.line_cacheedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)check_eq(Editor_state.cursor1.line, 1, 'F - test_move_cursor_using_mouse/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_move_cursor_using_mouse/cursor:pos')endfunction test_pagedown()io.write('\ntest_pagedown')App.screen.init{width=120, height=45}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- initially the first two lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_pagedown/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_pagedown/baseline/screen:2')-- after pagedown the bottom line becomes the topedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 2, 'F - test_pagedown/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_pagedown/cursor')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_pagedown/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_pagedown/screen:2')end
endfunction test_pagedown_can_start_from_middle_of_long_wrapping_line()io.write('\ntest_pagedown_can_start_from_middle_of_long_wrapping_line')-- draw a few lines starting from a very long wrapping lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi jkl mno pqr stu vwx yza bcd efg hij', 'XYZ'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:3')-- after pagedown we scroll down the very long wrapping lineedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen_top:line')check_eq(Editor_state.screen_top1.pos, 9, 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen_top:pos')y = Editor_state.topApp.screen.check(y, 'ghi ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:3')endfunction test_pagedown_never_moves_up()io.write('\ntest_pagedown_never_moves_up')-- draw the final screen line of a wrapping lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=9}Editor_state.screen_top1 = {line=1, pos=9}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- pagedown makes no changeedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pagedown_never_moves_up/screen_top:line')check_eq(Editor_state.screen_top1.pos, 9, 'F - test_pagedown_never_moves_up/screen_top:pos')endfunction test_down_arrow_moves_cursor()io.write('\ntest_down_arrow_moves_cursor')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- initially the first three lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_down_arrow_moves_cursor/baseline/screen:3')-- after hitting the down arrow, the cursor moves down by 1 lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 1, 'F - test_down_arrow_moves_cursor/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_down_arrow_moves_cursor/cursor')-- the screen is unchangedy = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_down_arrow_moves_cursor/screen:3')endfunction test_down_arrow_scrolls_down_by_one_line()io.write('\ntest_down_arrow_scrolls_down_by_one_line')-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:3')-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_line/screen_top')check_eq(Editor_state.cursor1.line, 4, 'F - test_down_arrow_scrolls_down_by_one_line/cursor')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_down_arrow_scrolls_down_by_one_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_down_arrow_scrolls_down_by_one_line/screen:3')endfunction test_down_arrow_scrolls_down_by_one_screen_line()io.write('\ntest_down_arrow_scrolls_down_by_one_screen_line')-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_down_arrow_scrolls_down_by_one_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_down_arrow_scrolls_down_by_one_screen_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:3')endfunction test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word()io.write('\ntest_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word')-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/baseline/screen:3')-- after hitting the down arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 2, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:3')endfunction test_page_down_followed_by_down_arrow_does_not_scroll_screen_up()io.write('\ntest_page_down_followed_by_down_arrow_does_not_scroll_screen_up')App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghijkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline/screen:3')-- after hitting pagedown the screen scrolls down to start of a long lineedit.run_after_keychord(Editor_state, 'pagedown')check_eq(Editor_state.screen_top1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/baseline2/cursor:pos')-- after hitting down arrow the screen doesn't scroll down further, and certainly doesn't scroll upedit.run_after_keychord(Editor_state, 'down')check_eq(Editor_state.screen_top1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/cursor:pos')y = Editor_state.topApp.screen.check(y, 'ghij', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:3')endfunction test_up_arrow_moves_cursor()io.write('\ntest_up_arrow_moves_cursor')-- display the first 3 lines with the cursor on the bottom lineApp.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_moves_cursor/baseline/screen:3')-- after hitting the up arrow the cursor moves up by 1 lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_moves_cursor/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_up_arrow_moves_cursor/cursor')-- the screen is unchangedy = Editor_state.topApp.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_moves_cursor/screen:3')endfunction test_up_arrow_scrolls_up_by_one_line()io.write('\ntest_up_arrow_scrolls_up_by_one_line')-- display the lines 2/3/4 with the cursor on line 2App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:3')-- after hitting the up arrow the screen scrolls up by one lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_by_one_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_by_one_line/cursor')y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_by_one_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/screen:3')endfunction test_up_arrow_scrolls_up_by_one_screen_line()io.write('\ntest_up_arrow_scrolls_up_by_one_screen_line')-- display lines starting from second screen line of a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=6}Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_by_one_screen_line/baseline/screen:2')-- after hitting the up arrow the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'up')y = Editor_state.topApp.screen.check(y, 'ghi ', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:3')check_eq(Editor_state.screen_top1.line, 3, 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen_top')check_eq(Editor_state.screen_top1.pos, 1, 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_up_arrow_scrolls_up_by_one_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_up_arrow_scrolls_up_by_one_screen_line/cursor:pos')endfunction test_up_arrow_scrolls_up_to_final_screen_line()io.write('\ntest_up_arrow_scrolls_up_to_final_screen_line')-- display lines starting just after a long lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:3')-- after hitting the up arrow the screen scrolls up to final screen line of previous lineedit.run_after_keychord(Editor_state, 'up')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:3')check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen_top')check_eq(Editor_state.screen_top1.pos, 5, 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_to_final_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_up_arrow_scrolls_up_to_final_screen_line/cursor:pos')endfunction test_up_arrow_scrolls_up_to_empty_line()io.write('\ntest_up_arrow_scrolls_up_to_empty_line')-- display a screenful of text with an empty line just above it outside the screenApp.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'', 'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:3')-- after hitting the up arrow the screen scrolls up by one lineedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.screen_top1.line, 1, 'F - test_up_arrow_scrolls_up_to_empty_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_up_arrow_scrolls_up_to_empty_line/cursor')y = Editor_state.top-- empty first liney = y + Editor_state.line_heightApp.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:3')endfunction test_pageup()io.write('\ntest_pageup')App.screen.init{width=120, height=45}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}-- initially the last two lines are displayededit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'F - test_pageup/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_pageup/baseline/screen:2')-- after pageup the cursor goes to first lineedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup/cursor')y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_pageup/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_pageup/screen:2')endfunction test_pageup_scrolls_up_by_screen_line()io.write('\ntest_pageup_scrolls_up_by_screen_line')-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi', 'jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the page-up key the screen scrolls up to topedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup_scrolls_up_by_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup_scrolls_up_by_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_pageup_scrolls_up_by_screen_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc ', 'F - test_pageup_scrolls_up_by_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_pageup_scrolls_up_by_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/screen:3')endfunction test_pageup_scrolls_up_from_middle_screen_line()io.write('\ntest_pageup_scrolls_up_from_middle_screen_line')-- display a few lines starting from the middle of a line (Editor_state.cursor1.pos > 1)App.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=5}Editor_state.screen_top1 = {line=2, pos=5}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_from_middle_screen_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_pageup_scrolls_up_from_middle_screen_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the page-up key the screen scrolls up to topedit.run_after_keychord(Editor_state, 'pageup')check_eq(Editor_state.screen_top1.line, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_pageup_scrolls_up_from_middle_screen_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:3')endfunction test_enter_on_bottom_line_scrolls_down()io.write('\ntest_enter_on_bottom_line_scrolls_down')-- display a few lines with cursor on bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:3')-- after hitting the enter key the screen scrolls downedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.screen_top1.line, 2, 'F - test_enter_on_bottom_line_scrolls_down/screen_top')check_eq(Editor_state.cursor1.line, 4, 'F - test_enter_on_bottom_line_scrolls_down/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_enter_on_bottom_line_scrolls_down/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'g', 'F - test_enter_on_bottom_line_scrolls_down/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'hi', 'F - test_enter_on_bottom_line_scrolls_down/screen:3')endfunction test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom()io.write('\ntest_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom')-- display just the bottom line on screenApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=4, pos=2}Editor_state.screen_top1 = {line=4, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/baseline/screen:1')-- after hitting the enter key the screen does not scroll downedit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.screen_top1.line, 4, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen_top')check_eq(Editor_state.cursor1.line, 5, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:pos')y = Editor_state.topApp.screen.check(y, 'j', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:2')endfunction test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom()io.write('\ntest_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom')-- display just an empty bottom line on screenApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', ''}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- after hitting the inserting_text key the screen does not scroll downedit.run_after_textinput(Editor_state, 'a')check_eq(Editor_state.screen_top1.line, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen_top')check_eq(Editor_state.cursor1.line, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:line')check_eq(Editor_state.cursor1.pos, 2, 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/cursor:pos')local y = Editor_state.topApp.screen.check(y, 'a', 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')endfunction test_typing_on_bottom_line_scrolls_down()io.write('\ntest_typing_on_bottom_line_scrolls_down')-- display a few lines with cursor on bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=4}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:3')-- after typing something the line wraps and the screen scrolls downedit.run_after_textinput(Editor_state, 'j')edit.run_after_textinput(Editor_state, 'k')edit.run_after_textinput(Editor_state, 'l')check_eq(Editor_state.screen_top1.line, 2, 'F - test_typing_on_bottom_line_scrolls_down/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_typing_on_bottom_line_scrolls_down/cursor:line')check_eq(Editor_state.cursor1.pos, 7, 'F - test_typing_on_bottom_line_scrolls_down/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghij', 'F - test_typing_on_bottom_line_scrolls_down/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_typing_on_bottom_line_scrolls_down/screen:3')endfunction test_left_arrow_scrolls_up_in_wrapped_line()io.write('\ntest_left_arrow_scrolls_up_in_wrapped_line')-- display lines starting from second screen line of a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}-- cursor is at top of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_left_arrow_scrolls_up_in_wrapped_line/baseline/screen:2')-- after hitting the left arrow the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'left')y = Editor_state.topApp.screen.check(y, 'ghi ', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:3')check_eq(Editor_state.screen_top1.line, 3, 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen_top')check_eq(Editor_state.screen_top1.pos, 1, 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_left_arrow_scrolls_up_in_wrapped_line/cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_left_arrow_scrolls_up_in_wrapped_line/cursor:pos')endfunction test_right_arrow_scrolls_down_in_wrapped_line()io.write('\ntest_right_arrow_scrolls_down_in_wrapped_line')-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- cursor is at bottom right of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting the right arrow the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'right')check_eq(Editor_state.screen_top1.line, 2, 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_right_arrow_scrolls_down_in_wrapped_line/cursor:line')check_eq(Editor_state.cursor1.pos, 6, 'F - test_right_arrow_scrolls_down_in_wrapped_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:3')endfunction test_home_scrolls_up_in_wrapped_line()io.write('\ntest_home_scrolls_up_in_wrapped_line')-- display lines starting from second screen line of a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}-- cursor is at top of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_home_scrolls_up_in_wrapped_line/baseline/screen:2')-- after hitting home the screen scrolls up to first screen lineedit.run_after_keychord(Editor_state, 'home')y = Editor_state.topApp.screen.check(y, 'ghi ', 'F - test_home_scrolls_up_in_wrapped_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_home_scrolls_up_in_wrapped_line/screen:3')check_eq(Editor_state.screen_top1.line, 3, 'F - test_home_scrolls_up_in_wrapped_line/screen_top')check_eq(Editor_state.screen_top1.pos, 1, 'F - test_home_scrolls_up_in_wrapped_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_home_scrolls_up_in_wrapped_line/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_home_scrolls_up_in_wrapped_line/cursor:pos')endfunction test_end_scrolls_down_in_wrapped_line()io.write('\ntest_end_scrolls_down_in_wrapped_line')-- display the first three lines with the cursor on the bottom lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- cursor is at bottom right of screenEditor_state.cursor1 = {line=3, pos=5}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:3') -- line wrapping includes trailing whitespace-- after hitting end the screen scrolls down by one lineedit.run_after_keychord(Editor_state, 'end')check_eq(Editor_state.screen_top1.line, 2, 'F - test_end_scrolls_down_in_wrapped_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_end_scrolls_down_in_wrapped_line/cursor:line')check_eq(Editor_state.cursor1.pos, 8, 'F - test_end_scrolls_down_in_wrapped_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi ', 'F - test_end_scrolls_down_in_wrapped_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_end_scrolls_down_in_wrapped_line/screen:3')endfunction test_position_cursor_on_recently_edited_wrapping_line()-- draw a line wrapping over 2 screen linesio.write('\ntest_position_cursor_on_recently_edited_wrapping_line')App.screen.init{width=100, height=200}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc def ghi jkl mno pqr ', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=25}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:3')-- add to the line until it's wrapping over 3 screen linesedit.run_after_textinput(Editor_state, 's')edit.run_after_textinput(Editor_state, 't')edit.run_after_textinput(Editor_state, 'u')check_eq(Editor_state.cursor1.pos, 28, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'stu', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:3')-- try to move the cursor earlier in the third screen line by clicking the mouseedit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)-- cursor should movecheck_eq(Editor_state.cursor1.line, 1, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:line')check_eq(Editor_state.cursor1.pos, 26, 'F - test_position_cursor_on_recently_edited_wrapping_line/cursor:pos')endfunction test_backspace_can_scroll_up()io.write('\ntest_backspace_can_scroll_up')-- display the lines 2/3/4 with the cursor on line 2App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi', 'jkl'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}Editor_state.screen_top1 = {line=2, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'def', 'F - test_backspace_can_scroll_up/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/baseline/screen:3')-- after hitting backspace the screen scrolls up by one lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.screen_top1.line, 1, 'F - test_backspace_can_scroll_up/screen_top')check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_can_scroll_up/cursor')y = Editor_state.topApp.screen.check(y, 'abcdef', 'F - test_backspace_can_scroll_up/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/screen:3')endfunction test_backspace_can_scroll_up_screen_line()io.write('\ntest_backspace_can_scroll_up_screen_line')-- display lines starting from second screen line of a lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'ghi jkl', 'mno'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=3, pos=5}Editor_state.screen_top1 = {line=3, pos=5}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)local y = Editor_state.topApp.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:2')-- after hitting backspace the screen scrolls up by one screen lineedit.run_after_keychord(Editor_state, 'backspace')y = Editor_state.topApp.screen.check(y, 'ghij', 'F - test_backspace_can_scroll_up_screen_line/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'kl', 'F - test_backspace_can_scroll_up_screen_line/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/screen:3')check_eq(Editor_state.screen_top1.line, 3, 'F - test_backspace_can_scroll_up_screen_line/screen_top')check_eq(Editor_state.screen_top1.pos, 1, 'F - test_backspace_can_scroll_up_screen_line/screen_top')check_eq(Editor_state.cursor1.line, 3, 'F - test_backspace_can_scroll_up_screen_line/cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_backspace_can_scroll_up_screen_line/cursor:pos')endfunction test_backspace_past_line_boundary()io.write('\ntest_backspace_past_line_boundary')-- position cursor at start of a (non-first) lineApp.screen.init{width=Editor_state.left+30, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=1}-- backspace joins with previous lineedit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary")endfunction test_undo_insert_text()io.write('\ntest_undo_insert_text')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'def', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=4}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- insert a characteredit.draw(Editor_state)edit.run_after_textinput(Editor_state, 'g')check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/baseline/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_insert_text/baseline/cursor:pos')local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3')-- undoedit.run_after_keychord(Editor_state, 'C-z')check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_insert_text/cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_insert_text/cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_undo_insert_text/screen:3')endfunction test_undo_delete_text()io.write('\ntest_undo_delete_text')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc', 'defg', 'xyz'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=2, pos=5}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}-- delete a characteredit.run_after_keychord(Editor_state, 'backspace')check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/baseline/cursor:line')check_eq(Editor_state.cursor1.pos, 4, 'F - test_undo_delete_text/baseline/cursor:pos')local y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3')-- undo--? -- after undo, the backspaced key is selectededit.run_after_keychord(Editor_state, 'C-z')check_eq(Editor_state.cursor1.line, 2, 'F - test_undo_delete_text/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_undo_delete_text/cursor:pos')y = Editor_state.topApp.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1')y = y + Editor_state.line_heightApp.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2')y = y + Editor_state.line_heightApp.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')endfunction test_search()io.write('\ntest_search')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()
Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'd')edit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.cursor1.line, 2, 'F - test_search/1/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_search/1/cursor:pos')-- reset cursorEditor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}-- search for second occurrenceedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'de')edit.run_after_keychord(Editor_state, 'down')edit.run_after_keychord(Editor_state, 'return')check_eq(Editor_state.cursor1.line, 4, 'F - test_search/2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_search/2/cursor:pos')endfunction test_search_upwards()io.write('\ntest_search_upwards')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc abd'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=2}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'a')-- search for previous occurrenceedit.run_after_keychord(Editor_state, 'up')check_eq(Editor_state.cursor1.line, 1, 'F - test_search_upwards/2/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_search_upwards/2/cursor:pos')endfunction test_search_wrap()io.write('\ntest_search_wrap')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=3}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'a')edit.run_after_keychord(Editor_state, 'return')-- cursor wrapscheck_eq(Editor_state.cursor1.line, 1, 'F - test_search_wrap/1/cursor:line')check_eq(Editor_state.cursor1.pos, 1, 'F - test_search_wrap/1/cursor:pos')endfunction test_search_wrap_upwards()io.write('\ntest_search_wrap_upwards')App.screen.init{width=120, height=60}Editor_state = edit.initialize_test_state()Editor_state.lines = load_array{'abc abd'}Text.redraw_all(Editor_state)Editor_state.cursor1 = {line=1, pos=1}Editor_state.screen_top1 = {line=1, pos=1}Editor_state.screen_bottom1 = {}edit.draw(Editor_state)-- search upwards for a stringedit.run_after_keychord(Editor_state, 'C-f')edit.run_after_textinput(Editor_state, 'a')edit.run_after_keychord(Editor_state, 'up')-- cursor wrapscheck_eq(Editor_state.cursor1.line, 1, 'F - test_search_wrap_upwards/1/cursor:line')check_eq(Editor_state.cursor1.pos, 5, 'F - test_search_wrap_upwards/1/cursor:pos')end
if line.mode ~= 'text' then return endif line.mode ~= 'text' then return endif 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.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].dataBendtable.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.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)endState.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos, posB=State.screen_bottom1.posB}assert(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' 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) - 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' 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) - 1breakendassert(State.lines[State.cursor1.line].mode == 'text')local 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)breakendState.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB} -- copyelselocal 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,}breakendendassert(State.lines[State.cursor1.line].mode == 'text')elselocal 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}breakendendelselocal 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}breakendendlocal 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 thenif 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 - hif State.lines[loc1.line].mode == 'drawing' thenreturn {line=loc1.line, screen_line=1, screen_pos=1}end
-- text editor, particularly text drawing, horizontal wrap, vertical scrollingText = {}AB_padding = 20 -- space in pixels between A side and B side-- draw a line starting from startpos to screen at y between State.left and State.right-- return the final y, and pos,posB of start of final screen line drawnfunction Text.draw(State, line_index, y, startpos, startposB)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]line_cache.starty = yline_cache.startpos = startposline_cache.startposB = startposB-- draw A sidelocal overflows_screen, x, pos, screen_line_starting_posif startpos thenoverflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_line(State, line_index, State.left, y, startpos)if overflows_screen thenreturn y, screen_line_starting_posendif Focus == 'edit' and State.cursor1.pos thenif State.search_term == nil thenif line_index == State.cursor1.line and State.cursor1.pos == pos thenText.draw_cursor(State, x, y)endendendelsex = State.leftend-- check for B side--? if line_index == 8 then print('checking for B side') endif line.dataB == nil thenassert(y)assert(screen_line_starting_pos)--? if line_index == 8 then print('return 1') endreturn y, screen_line_starting_posendif not State.expanded and not line.expanded thenassert(y)assert(screen_line_starting_pos)--? if line_index == 8 then print('return 2') endbutton(State, 'expand', {x=x+AB_padding, y=y+2, w=App.width(State.em), h=State.line_height-4, color={1,1,1},icon = function(button_params)App.color(Fold_background_color)love.graphics.rectangle('fill', button_params.x, button_params.y, App.width(State.em), State.line_height-4, 2,2)end,onpress1 = function()line.expanded = trueend,})return y, screen_line_starting_posend-- draw B side--? if line_index == 8 then print('drawing B side') endApp.color(Fold_color)if startposB thenoverflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB)elseoverflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x+AB_padding,y, 1)endif overflows_screen thenreturn y, nil, screen_line_starting_posend--? if line_index == 8 then print('a') endif Focus == 'edit' and State.cursor1.posB then--? if line_index == 8 then print('b') endif State.search_term == nil then--? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) endif line_index == State.cursor1.line and State.cursor1.posB == pos thenText.draw_cursor(State, x, y)endendendreturn y, nil, screen_line_starting_posend-- Given an array of fragments, draw the subset starting from pos to screen-- starting from (x,y).-- Return:-- - whether we got to bottom of screen before end of line-- - the final (x,y)-- - the final pos-- - starting pos of the final screen line drawnfunction Text.draw_wrapping_line(State, line_index, x,y, startpos)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]--? print('== line', line_index, '^'..line.data..'$')local screen_line_starting_pos = startposText.compute_fragments(State, line_index)local pos = 1initialize_color()for _, f in ipairs(line_cache.fragments) doApp.color(Text_color)local frag, frag_text = f.data, f.textselect_color(frag)local frag_len = utf8.len(frag)--? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)if pos < startpos then-- render nothing--? print('skipping', frag)else-- render fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenassert(x > State.left) -- no overfull linesy = y + State.line_heightif y + State.line_height > App.screen.height thenreturn --[[screen filled]] true, x,y, pos, screen_line_starting_posendscreen_line_starting_pos = posx = State.leftendApp.screen.draw(frag_text, x,y)-- render cursor if necessaryif State.cursor1.pos and line_index == State.cursor1.line thenif pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos thenif State.search_term thenif State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term thenlocal lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term))App.color(Text_color)love.graphics.print(State.search_term, x+lo_px,y)endelseif Focus == 'edit' thenText.draw_cursor(State, x+Text.x(frag, State.cursor1.pos-pos+1), y)App.color(Text_color)endendendx = x + frag_widthendpos = pos + frag_lenendreturn false, x,y, pos, screen_line_starting_posendfunction Text.draw_wrapping_lineB(State, line_index, x,y, startpos)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]local screen_line_starting_pos = startposText.compute_fragmentsB(State, line_index, x)local pos = 1for _, f in ipairs(line_cache.fragmentsB) dolocal frag, frag_text = f.data, f.textlocal frag_len = utf8.len(frag)--? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)if pos < startpos then-- render nothing--? print('skipping', frag)else-- render fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenassert(x > State.left) -- no overfull linesy = y + State.line_heightif y + State.line_height > App.screen.height thenreturn --[[screen filled]] true, x,y, pos, screen_line_starting_posendscreen_line_starting_pos = posx = State.leftendApp.screen.draw(frag_text, x,y)-- render cursor if necessaryif State.cursor1.posB and line_index == State.cursor1.line thenif pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB thenif State.search_term thenif State.lines[State.cursor1.line].dataB:sub(State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term)-1) == State.search_term thenlocal lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term))App.color(Fold_color)love.graphics.print(State.search_term, x+lo_px,y)endelseif Focus == 'edit' thenText.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y)App.color(Fold_color)endendendx = x + frag_widthendpos = pos + frag_lenendreturn false, x,y, pos, screen_line_starting_posendfunction Text.draw_cursor(State, x, y)-- blink every 0.5sif math.floor(Cursor_time*2)%2 == 0 thenApp.color(Cursor_color)love.graphics.rectangle('fill', x,y, 3,State.line_height)endState.cursor_x = xState.cursor_y = y+State.line_heightendfunction Text.populate_screen_line_starting_pos(State, line_index)local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]if line_cache.screen_line_starting_pos thenreturnend-- duplicate some logic from Text.drawText.compute_fragments(State, line_index)line_cache.screen_line_starting_pos = {1}local x = State.leftlocal pos = 1for _, f in ipairs(line_cache.fragments) dolocal frag, frag_text = f.data, f.text-- render fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenx = State.lefttable.insert(line_cache.screen_line_starting_pos, pos)endx = x + frag_widthlocal frag_len = utf8.len(frag)pos = pos + frag_lenendendfunction Text.compute_fragments(State, line_index)--? print('compute_fragments', line_index, 'between', State.left, State.right)local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]if line_cache.fragments thenreturnendline_cache.fragments = {}local x = State.left-- try to wrap at word boundariesfor frag in line.data:gmatch('%S*%s*') dolocal frag_text = App.newText(love.graphics.getFont(), frag)local frag_width = App.width(frag_text)--? print('x: '..tostring(x)..'; frag_width: '..tostring(frag_width)..'; '..tostring(State.right-x)..'px to go')while x + frag_width > State.right do--? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))if (x-State.left) < 0.8 * (State.right-State.left) then--? print('splitting')-- long word; chop it at some letter-- We're not going to reimplement TeX here.local bpos = Text.nearest_pos_less_than(frag, State.right - x)--? print('bpos', bpos)if bpos == 0 then break end -- avoid infinite loop when window is too narrowlocal boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos--? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')local frag1 = string.sub(frag, 1, boffset-1)local frag1_text = App.newText(love.graphics.getFont(), frag1)local frag1_width = App.width(frag1_text)--? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')assert(x + frag1_width <= State.right)table.insert(line_cache.fragments, {data=frag1, text=frag1_text})frag = string.sub(frag, boffset)frag_text = App.newText(love.graphics.getFont(), frag)frag_width = App.width(frag_text)endx = State.left -- new lineendif #frag > 0 then--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')table.insert(line_cache.fragments, {data=frag, text=frag_text})endx = x + frag_widthendendfunction Text.populate_screen_line_starting_posB(State, line_index, x)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line_cache.screen_line_starting_posB thenreturnend-- duplicate some logic from Text.drawText.compute_fragmentsB(State, line_index, x)line_cache.screen_line_starting_posB = {1}local pos = 1for _, f in ipairs(line_cache.fragmentsB) dolocal frag, frag_text = f.data, f.text-- render fragmentlocal frag_width = App.width(frag_text)if x + frag_width > State.right thenx = State.lefttable.insert(line_cache.screen_line_starting_posB, pos)endx = x + frag_widthlocal frag_len = utf8.len(frag)pos = pos + frag_lenendendfunction Text.compute_fragmentsB(State, line_index, x)--? print('compute_fragmentsB', line_index, 'between', x, State.right)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line_cache.fragmentsB thenreturnendline_cache.fragmentsB = {}-- try to wrap at word boundariesfor frag in line.dataB:gmatch('%S*%s*') dolocal frag_text = App.newText(love.graphics.getFont(), frag)local frag_width = App.width(frag_text)--? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')while x + frag_width > State.right do--? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))if (x-State.left) < 0.8 * (State.right-State.left) then--? print('splitting')-- long word; chop it at some letter-- We're not going to reimplement TeX here.local bpos = Text.nearest_pos_less_than(frag, State.right - x)--? print('bpos', bpos)if bpos == 0 then break end -- avoid infinite loop when window is too narrowlocal boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos--? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')local frag1 = string.sub(frag, 1, boffset-1)local frag1_text = App.newText(love.graphics.getFont(), frag1)local frag1_width = App.width(frag1_text)--? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')assert(x + frag1_width <= State.right)table.insert(line_cache.fragmentsB, {data=frag1, text=frag1_text})frag = string.sub(frag, boffset)frag_text = App.newText(love.graphics.getFont(), frag)frag_width = App.width(frag_text)endx = State.left -- new lineendif #frag > 0 then--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')table.insert(line_cache.fragmentsB, {data=frag, text=frag_text})endx = x + frag_widthendendfunction Text.textinput(State, t)if App.mouse_down(1) then return endif App.ctrl_down() or App.alt_down() or App.cmd_down() then return endlocal before = snapshot(State, State.cursor1.line)--? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)Text.insert_at_cursor(State, t)if State.cursor_y > App.screen.height - State.line_height thenText.populate_screen_line_starting_pos(State, State.cursor1.line)Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endrecord_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})endfunction Text.insert_at_cursor(State, t)if State.cursor1.pos thenlocal byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)Text.clear_screen_line_cache(State, State.cursor1.line)State.cursor1.pos = State.cursor1.pos+1elseassert(State.cursor1.posB)local byte_offset = Text.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].dataB, byte_offset)Text.clear_screen_line_cache(State, State.cursor1.line)State.cursor1.posB = State.cursor1.posB+1endend-- Don't handle any keys here that would trigger love.textinput above.function Text.keychord_pressed(State, chord)--? print('chord', chord)--== shortcuts that mutate textif chord == 'return' thenlocal before_line = State.cursor1.linelocal before = snapshot(State, before_line)Text.insert_return(State)if State.cursor_y > App.screen.height - State.line_height thenText.snap_cursor_to_bottom_of_screen(State, State.left, State.right)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})elseif chord == 'tab' thenlocal before = snapshot(State, State.cursor1.line)--? print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)Text.insert_at_cursor(State, '\t')if State.cursor_y > App.screen.height - State.line_height thenText.populate_screen_line_starting_pos(State, State.cursor1.line)Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)--? print('=>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)endschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})elseif chord == 'backspace' thenlocal beforeif State.cursor1.pos and State.cursor1.pos > 1 thenbefore = snapshot(State, State.cursor1.line)local byte_start = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos-1)local byte_end = utf8.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)if byte_start thenif byte_end thenState.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].data, byte_end)elseState.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)endState.cursor1.pos = State.cursor1.pos-1endelseif State.cursor1.posB thenif State.cursor1.posB > 1 thenbefore = snapshot(State, State.cursor1.line)local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1)local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)if byte_start thenif byte_end thenState.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)elseState.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)endState.cursor1.posB = State.cursor1.posB-1endelse-- refuse to delete past beginning of side Bendelseif State.cursor1.line > 1 thenbefore = snapshot(State, State.cursor1.line-1, State.cursor1.line)
endState.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.pos
elseif State.cursor1.posB thenif State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) thenlocal byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB+1)if byte_start thenif byte_end thenState.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)elseState.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)end-- no change to State.cursor1.posendelse-- refuse to delete past end of side Bendelseif State.cursor1.line < #State.lines then
table.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 end
top2 = 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_top1else
end--? print('setting top to', State.screen_top1.line, State.screen_top1.pos)State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB}Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State)--? print('top now', State.screen_top1.line)Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks--? print('pagedown end')endfunction Text.up(State)
if State.cursor1.pos thenText.upA(State)elseText.upB(State)endendfunction Text.upA(State)--? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)if screen_line_starting_pos == 1 then--? print('cursor is at first screen line of its line')-- line is done; skip to previous text line
endelse-- 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 line
endelseif 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) then
endendfunction 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)
Text.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-1else
endif Text.lt1(State.cursor1, State.screen_top1) thenlocal top2 = Text.to2(State, State.screen_top1)top2 = Text.previous_screen_line(State, top2)State.screen_top1 = Text.to1(State, top2)endendfunction Text.leftB(State)if State.cursor1.posB > 1 thenState.cursor1.posB = State.cursor1.posB-1else-- overflow back into A sideState.cursor1.posB = nilState.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1endif Text.lt1(State.cursor1, State.screen_top1) thenlocal top2 = Text.to2(State, State.screen_top1)top2 = Text.previous_screen_line(State, top2)State.screen_top1 = Text.to1(State, top2)endendfunction Text.right(State)Text.right_without_scroll(State)if Text.cursor_out_of_screen(State) thenText.snap_cursor_to_bottom_of_screen(State)endendfunction Text.right_without_scroll(State)
endendendfunction 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,i
endassert(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 end
endtop2 = 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)
endfunction 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+$', '')end
if line == '```lines' then -- inflexible with whitespace since these files are always autogeneratedtable.insert(result, load_drawing(infile_next_line))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)table.insert(result, {mode='text', data=''})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')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')--? 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)--? 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)table.insert(result, {mode='text', data=''})function 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, drawingend
-- 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 end
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 editedIcon_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}Drawing_padding_top = 10Drawing_padding_bottom = 10Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottomSame_point_distance = 4 -- pixel distance at which two points are considered the same-- 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 linescurrent_drawing_mode = 'line',previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming pointsfunction edit.fixup_cursor(State)for i,line in ipairs(State.lines) doif line.mode == 'text' thenState.cursor1.line = ibreakendendend--? print('draw:', y, line_index, line, line.mode)if line.mode == 'text' thenlocal 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,})y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB)y = 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)Drawing.update(State, dt)--? print('press')if line.mode == 'text' thenif Text.in_line(State, line_index, x,y) thenlocal pos,posB = Text.to_pos_on_line(State, line_index, x, y)--? print(x,y, 'setting cursor:', line_index, pos, posB)State.cursor1 = {line=line_index, pos=pos, posB=posB}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)breakendif 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 = nilendendelseif 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)})elseif chord == 'C-i' then-- invalidate various cached bits of linesState.lines.current_drawing = nil-- invalidate various cached bits of linesState.lines.current_drawing = nil-- 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)
-- 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,
function edit.draw(State)State.button_handlers = {}App.color(Text_color)assert(#State.lines == #State.line_cache)if not Text.le1(State.screen_top1, State.cursor1) thenprint(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB)assert(false)endState.cursor_x = nilState.cursor_y = nillocal y = State.top--? print('== draw')for line_index = State.screen_top1.line,#State.lines dolocal line = State.lines[line_index]
if State.next_save and State.next_save < App.getTime() thensave_to_disk(State)State.next_save = nilendendfunction schedule_save(State)if State.next_save == nil thenState.next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you didendendfunction edit.quit(State)-- make sure to save before quittingif State.next_save thensave_to_disk(State)endendfunction edit.mouse_pressed(State, x,y, mouse_button)if State.search_term then return end
elseText.textinput(State, t)endschedule_save(State)endfunction edit.keychord_pressed(State, chord, key)if State.search_term thenif chord == 'escape' thenState.search_term = nilState.search_text = nilState.cursor1 = State.search_backup.cursorState.screen_top1 = State.search_backup.screen_topState.search_backup = nilText.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leakselseif chord == 'return' thenState.search_term = nilState.search_text = nilState.search_backup = nilelseif chord == 'backspace' thenlocal len = utf8.len(State.search_term)local byte_offset = Text.offset(State.search_term, len)State.search_term = string.sub(State.search_term, 1, byte_offset-1)State.search_text = nilelseif chord == 'down' thenif State.cursor1.pos thenState.cursor1.pos = State.cursor1.pos+1elseState.cursor1.posB = State.cursor1.posB+1endText.search_next(State)elseif chord == 'up' thenText.search_previous(State)endreturnelseif chord == 'C-f' thenState.search_term = ''State.search_backup = {cursor={line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB},screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB},}assert(State.search_text == nil)-- bifold textelseif chord == 'C-b' thenState.expanded = not State.expandedText.redraw_all(State)if not State.expanded thenfor _,line in ipairs(State.lines) doline.expanded = nilendedit.eradicate_locations_after_the_fold(State)end
if 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,App.screen.width, -- right margin = 014, -- font height assuming default LÖVE font15) -- line heightend-- all textinput events are also keypresses-- TODO: handle chords of multiple keysfunction edit.run_after_textinput(State, t)edit.keychord_pressed(State, t)edit.textinput(State, t)edit.key_released(State, t)App.screen.contents = {}edit.draw(State)end-- not all keys are textinputfunction edit.run_after_keychord(State, chord)edit.keychord_pressed(State, chord)edit.key_released(State, chord)App.screen.contents = {}edit.draw(State)endfunction edit.run_after_mouse_click(State, x,y, mouse_button)App.fake_mouse_press(x,y, mouse_button)edit.mouse_pressed(State, x,y, mouse_button)App.fake_mouse_release(x,y, mouse_button)edit.mouse_released(State, x,y, mouse_button)App.screen.contents = {}edit.draw(State)endfunction edit.run_after_mouse_press(State, x,y, mouse_button)App.fake_mouse_press(x,y, mouse_button)edit.mouse_pressed(State, x,y, mouse_button)App.screen.contents = {}edit.draw(State)endfunction edit.run_after_mouse_release(State, x,y, mouse_button)App.fake_mouse_release(x,y, mouse_button)edit.mouse_released(State, x,y, mouse_button)App.screen.contents = {}edit.draw(State)end
elseif Current_app == 'source' thenload_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')elseassert(false, 'unknown app "'..Current_app..'"')
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