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 operations
end
end
return result
end
end
return result
end
end
-- Make copies of objects; the rest of the app may mutate them in place, but undo requires immutable histories.
-- compare with App.initialize_globals
local event = {
lines={},
-- no filename; undo history is cleared when filename changes
}
-- deep copy lines without cached stuff like text fragments
end
return event
end
-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
function deepcopy(obj, seen)
if type(obj) ~= 'table' then return obj end
if seen and seen[obj] then return seen[obj] end
local s = seen or {}
local result = setmetatable({}, getmetatable(obj))
s[obj] = result
for k,v in pairs(obj) do
result[deepcopy(k, s)] = deepcopy(v, s)
end
return result
end
function minmax(a, b)
return math.min(a,b), math.max(a,b)
end
function 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
--? end
assert(from.start_line == to.start_line)
for i=from.end_line,from.start_line,-1 do
table.remove(lines, i)
end
assert(#to.lines == to.end_line-to.start_line+1)
for i=1,#to.lines do
table.insert(lines, to.start_line+i-1, to.lines[i])
end
end
function patch_placeholders(line_cache, from, to)
assert(from.start_line == to.start_line)
for i=from.end_line,from.start_line,-1 do
table.remove(line_cache, i)
end
assert(#to.lines == to.end_line-to.start_line+1)
for i=1,#to.lines do
table.insert(line_cache, to.start_line+i-1, {})
end
end
for i=s,e do
local 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 = s
end
if s < 1 then s = 1 end
if e < 1 then e = 1 end
if e > #State.lines then e = #State.lines end
if s > #State.lines then s = #State.lines end
assert(#State.lines > 0)
assert(s)
if e == nil then
function 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+1
function undo_event(State)
if State.next_history > 1 then
--? print('moving to history', State.next_history-1)
State.next_history = State.next_history-1
local result = State.history[State.next_history]
function record_undo_event(State, data)
State.history[State.next_history] = data
State.next_history = State.next_history+1
for i=State.next_history,#State.history do
State.history[i] = nil
-- major tests for text editing flows
function 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')
end
function 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 line
end
function 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')
end
function 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')
end
function 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')
end
function 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')
end
function 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')
end
function 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')
end
function 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')
end
function 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')
end
function 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')
end
function 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')
end
Editor_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')
end
function 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')
end
Editor_state.lines = load_array{'abc def'}
Editor_state.cursor1 = {line=1, pos=4} -- at the start of second word
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\tdef'}
Editor_state.cursor1 = {line=1, pos=1} -- at the space between words
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'}
Editor_state.cursor1 = {line=1, pos=4} -- at the space between words
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'}
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 word
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\tghi'}
Editor_state.cursor1 = {line=1, pos=10} -- within third word
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=5} -- at the start of second word
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} -- at the space between words
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'}
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 line
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{'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')
end
function 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_height
end
function 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')
end
function 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 them
App.screen.init{width=100, height=80}
Editor_state = edit.initialize_test_state()
Editor_state.left = 50 -- occupy only right side of screen
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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
-- cursor moves
check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_wrapping_line_takes_margins_into_account/cursor:line')
end
check_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 word
io.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_height
end
function test_draw_wrapping_text_containing_non_ascii()
-- draw a long line containing non-ASCII
io.write('\ntest_draw_wrapping_text_containing_non_ascii')
App.screen.init{width=60, height=60}
y = y + Editor_state.line_height
y = y + Editor_state.line_height
end
function 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_height
end
function 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')
end
function 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.top
App.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')
end
function test_move_cursor_using_mouse()
io.write('\ntest_move_cursor_using_mouse')
App.screen.init{width=50, height=60}
end
function test_pagedown()
io.write('\ntest_pagedown')
App.screen.init{width=120, height=45}
-- initially the first two lines are displayed
App.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 top
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.top
App.screen.check(y, 'def', 'F - test_pagedown/screen:1')
App.screen.check(y, 'ghi', 'F - test_pagedown/screen:2')
end
end
function 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 displayed
App.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 line
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 unchanged
App.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')
end
function 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 line
App.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 line
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.top
App.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')
end
function 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 line
Editor_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.top
Text.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 line
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')
y = Editor_state.top
check_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')
end
function 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 line
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.top
Text.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 line
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')
y = Editor_state.top
check_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_height
end
function 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.top
Text.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 line
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 up
y = y + Editor_state.line_height
y = y + Editor_state.line_height
App.screen.check(y, 'mno', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:3')
end
function test_up_arrow_moves_cursor()
io.write('\ntest_up_arrow_moves_cursor')
-- display the first 3 lines with the cursor on the bottom line
App.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 line
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 unchanged
App.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')
end
function 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 2
App.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 line
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.top
App.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')
end
function 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 line
Editor_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.top
Text.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 line
App.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')
end
function 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 line
Editor_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.top
Text.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 line
App.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')
end
function 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 screen
App.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 line
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 line
App.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')
end
function test_pageup()
io.write('\ntest_pageup')
App.screen.init{width=120, height=45}
-- initially the last two lines are displayed
App.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 line
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.top
App.screen.check(y, 'abc', 'F - test_pageup/screen:1')
App.screen.check(y, 'def', 'F - test_pageup/screen:2')
end
function 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 line
Editor_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.top
Text.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 top
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.top
App.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')
end
function 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 top
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.top
App.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')
end
function test_enter_on_bottom_line_scrolls_down()
io.write('\ntest_enter_on_bottom_line_scrolls_down')
-- display a few lines with cursor on bottom line
Editor_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.top
Text.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 down
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.top
App.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')
end
function 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 screen
Editor_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.top
Text.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 down
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.top
App.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')
end
function test_position_cursor_on_recently_edited_wrapping_line()
-- draw a line wrapping over 2 screen lines
io.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 lines
App.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 move
end
function test_backspace_can_scroll_up()
io.write('\ntest_backspace_can_scroll_up')
-- display the lines 2/3/4 with the cursor on line 2
App.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 line
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.top
App.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')
end
function 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 line
Editor_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.top
Text.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 line
y = y + Editor_state.line_height
y = y + Editor_state.line_height
App.screen.check(y, 'mno', 'F - test_backspace_can_scroll_up_screen_line/screen:3')
end
function test_backspace_past_line_boundary()
io.write('\ntest_backspace_past_line_boundary')
-- position cursor at start of a (non-first) line
Editor_state.lines = load_array{'abc', 'def'}
Editor_state.cursor1 = {line=2, pos=1}
Text.redraw_all(Editor_state)
-- backspace joins with previous line
end
function test_undo_insert_text()
io.write('\ntest_undo_insert_text')
App.screen.init{width=120, height=60}
-- insert a character
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')
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')
-- undo
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.top
App.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')
end
function test_undo_delete_text()
io.write('\ntest_undo_delete_text')
App.screen.init{width=120, height=60}
-- delete a character
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.top
App.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 selected
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.top
App.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 string
edit.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 cursor
Editor_state.cursor1 = {line=1, pos=1}
-- search for second occurrence
edit.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')
end
function 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 string
edit.run_after_keychord(Editor_state, 'C-f')
edit.run_after_textinput(Editor_state, 'a')
edit.run_after_keychord(Editor_state, 'return')
-- cursor wraps
check_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')
end
function 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 string
edit.run_after_keychord(Editor_state, 'C-f')
edit.run_after_textinput(Editor_state, 'a')
edit.run_after_keychord(Editor_state, 'up')
-- cursor wraps
check_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
function 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 string
edit.run_after_keychord(Editor_state, 'C-f')
edit.run_after_textinput(Editor_state, 'a')
-- search for previous occurrence
edit.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')
end
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.lines = load_array{'abc', 'def', 'ghi', 'deg'}
end
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'C-z')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'C-z')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
local y = Editor_state.top
edit.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.top
App.screen.check(y, 'ghij', 'F - test_backspace_can_scroll_up_screen_line/screen:1')
y = y + Editor_state.line_height
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'backspace')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
Editor_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.top
Text.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_height
y = y + Editor_state.line_height
y = Editor_state.top
edit.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_height
y = y + Editor_state.line_height
App.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.top
Text.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 screen
Editor_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 down
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.top
App.screen.check(y, 'a', 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')
end
edit.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 line
Editor_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.top
Text.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 down
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.top
App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/screen:1')
y = y + Editor_state.line_height
end
function 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 line
Editor_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 screen
App.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 line
App.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')
end
function 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 line
Editor_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 screen
App.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 line
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.top
App.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')
end
function 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 line
Editor_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 screen
App.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 line
App.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')
end
function 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 line
Editor_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 screen
App.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 line
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.top
App.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')
end
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'end')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
Editor_state.cursor1 = {line=3, pos=5}
edit.draw(Editor_state)
local y = Editor_state.top
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_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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'home')
y = Editor_state.top
y = y + Editor_state.line_height
Editor_state.cursor1 = {line=3, pos=5}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'right')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
Editor_state.cursor1 = {line=3, pos=5}
edit.draw(Editor_state)
local y = Editor_state.top
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_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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'left')
y = Editor_state.top
y = y + Editor_state.line_height
Editor_state.cursor1 = {line=3, pos=5}
edit.draw(Editor_state)
local y = Editor_state.top
App.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_height
App.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_height
y = y + Editor_state.line_height
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'return')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.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.top
Text.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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'pageup')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'pageup')
y = y + Editor_state.line_height
edit.draw(Editor_state)
local y = Editor_state.top
Editor_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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'up')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
Editor_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.top
Text.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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'up')
y = Editor_state.top
y = y + Editor_state.line_height
y = y + Editor_state.line_height
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_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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'up')
y = Editor_state.top
y = y + Editor_state.line_height
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'up')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
Editor_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.top
Text.redraw_all(Editor_state)
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
y = Editor_state.top
edit.run_after_keychord(Editor_state, 'up')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
Editor_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.top
Text.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.top
App.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_height
App.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_height
App.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_height
App.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_height
App.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_height
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'down')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'down')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
Editor_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.top
Text.redraw_all(Editor_state)
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
y = Editor_state.top
edit.run_after_keychord(Editor_state, 'down')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
edit.draw(Editor_state)
local y = Editor_state.top
Editor_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 line
App.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 change
edit.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')
end
function 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 line
Editor_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.top
Text.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 line
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.top
App.screen.check(y, 'ghi ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'mno ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:3')
y = y + Editor_state.line_height
App.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_height
y = y + Editor_state.line_height
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'pagedown')
y = y + Editor_state.line_height
edit.draw(Editor_state)
local y = Editor_state.top
Editor_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_cache
edit.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_height
y = y + Editor_state.line_height
edit.run_after_keychord(Editor_state, 'C-v')
-- paste some text including a newline, check that new line is created
y = y + Editor_state.line_height
y = y + Editor_state.line_height
-- display a few lines
Editor_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.top
Text.redraw_all(Editor_state)
App.screen.init{width=Editor_state.left+30, height=60}
Editor_state = edit.initialize_test_state()
end
function test_insert_newline_at_start_of_line()
io.write('\ntest_insert_newline_at_start_of_line')
-- display a line
Editor_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 line
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')
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_height
y = y + Editor_state.line_height
y = Editor_state.top
-- hitting the enter key splits the line
edit.run_after_keychord(Editor_state, 'return')
y = y + Editor_state.line_height
y = y + Editor_state.line_height
-- display a few lines
Editor_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.top
Text.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_height
App.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.top
Text.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 line
Editor_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 line
end
function test_click_past_end_of_wrapping_line()
io.write('\ntest_click_past_end_of_wrapping_line')
-- display a wrapping line
Editor_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_height
y = y + Editor_state.line_height
-- click past the end of it
-- cursor moves to end of line
end
-- display a wrapping line containing non-ASCII
Editor_state.lines = load_array{'madam I’m adam'} -- notice the non-ASCII apostrophe
Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
y = y + Editor_state.line_height
y = y + Editor_state.line_height
y = y + Editor_state.line_height
-- click past the end of it
-- cursor moves to end of line
end
function 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 length
Editor_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 line
end
check_eq(Editor_state.cursor1.pos, 20, 'F - test_click_past_end_of_word_wrapping_line/cursor')
-- click past the end of the screen line
edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
y = y + Editor_state.line_height
edit.draw(Editor_state)
local y = Editor_state.top
Text.redraw_all(Editor_state)
App.screen.init{width=160, height=80}
Editor_state = edit.initialize_test_state()
-- 0 1 2
-- 123456789012345678901
check_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-points
edit.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.top
App.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()
-- 12345678901234
function 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-points
edit.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_height
App.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.top
Text.redraw_all(Editor_state)
App.screen.init{width=75, height=80}
Editor_state = edit.initialize_test_state()
-- 12345678901234
function 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 line
Editor_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 line
end
check_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.top
App.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()
-- 12345678901234
check_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_height
App.screen.check(y, "I'm ad", 'F - test_click_on_wrapping_line/baseline/screen:2')
edit.draw(Editor_state)
local y = Editor_state.top
Text.redraw_all(Editor_state)
App.screen.init{width=75, height=80}
Editor_state = edit.initialize_test_state()
-- 12345678901234
App.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 apostrophe
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.top
App.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_height
App.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.top
Text.redraw_all(Editor_state)
Editor_state = edit.initialize_test_state()
end
function test_click_with_mouse_on_wrapping_line()
io.write('\ntest_click_with_mouse_on_wrapping_line')
-- display two lines with cursor on one of them
App.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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
-- cursor moves
check_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_height
y = y + Editor_state.line_height
Editor_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.top
Text.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_height
App.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.top
Text.redraw_all(Editor_state)
Editor_state = edit.initialize_test_state()
y = y + Editor_state.line_height
y = y + Editor_state.line_height
Editor_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.top
Text.redraw_all(Editor_state)
Editor_state = edit.initialize_test_state()
function test_click_with_mouse()
-- display two lines with cursor on one of them
App.screen.init{width=50, height=80}
-- click on the other line
-- cursor moves
end
end
function 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 them
App.screen.init{width=100, height=80}
Editor_state = edit.initialize_test_state()
Editor_state.left = 50 -- occupy only right side of screen
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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
-- cursor moves
check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_takes_margins_into_account/cursor:line')
end
check_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 empty
App.screen.init{width=50, height=80}
-- click on the empty line
-- cursor moves
check_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 middle
App.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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left-4,Editor_state.top+5, 1)
-- cursor moves to start of line
check_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')
end
check_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')
end
function test_press_ctrl()
io.write('\ntest_press_ctrl')
-- press ctrl while the cursor is on text
App.screen.init{width=50, height=80}
end
function test_move_left()
io.write('\ntest_move_left')
check_eq(Editor_state.cursor1.pos, 1, 'F - test_move_left')
end
function 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.top
Editor_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')
end
end
function 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 it
App.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 up
edit.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 = {}
end
end
end
end
-- Given an array of fragments, draw the subset starting from pos to screen
-- starting from (x,y).
-- Return:
-- - whether we got to bottom of screen before end of line
-- - the final (x,y)
-- - the final pos
-- - starting pos of the final screen line drawn
function Text.draw_wrapping_line(State, line_index, x,y, startpos)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
--? print('== line', line_index, '^'..line.data..'$')
local screen_line_starting_pos = startpos
for _, f in ipairs(line_cache.fragments) do
local frag, frag_text = f.data, f.text
local frag_len = utf8.len(frag)
--? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
if pos < startpos then
-- render nothing
--? print('skipping', frag)
else
-- render fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
assert(x > State.left) -- no overfull lines
y = y + State.line_height
if y + State.line_height > App.screen.height then
return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
end
screen_line_starting_pos = pos
x = State.left
end
App.screen.draw(frag_text, x,y)
-- render cursor if necessary
if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then
if State.search_term then
if State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then
local 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)
end
Text.draw_cursor(State, x+Text.x(frag, State.cursor1.pos-pos+1), y)
end
end
end
x = x + frag_width
end
pos = pos + frag_len
end
return false, x,y, pos, screen_line_starting_pos
end
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
local screen_line_starting_pos = startpos
local pos = 1
for _, f in ipairs(line_cache.fragmentsB) do
Text.compute_fragmentsB(State, line_index, x)
function Text.draw_wrapping_lineB(State, line_index, x,y, startpos)
App.color(Text_color)
elseif Focus == 'edit' then
if State.cursor1.pos and line_index == State.cursor1.line then
select_color(frag)
App.color(Text_color)
local frag, frag_text = f.data, f.text
y = y + State.line_height
if y + State.line_height > App.screen.height then
end
screen_line_starting_pos = pos
end
end
end
end
end
pos = pos + frag_len
end
return false, x,y, pos, screen_line_starting_pos
end
end
function Text.draw_cursor(State, x, y)
-- blink every 0.5s
if math.floor(Cursor_time*2)%2 == 0 then
end
State.cursor_x = x
State.cursor_y = y+State.line_height
end
function 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 then
return
end
-- duplicate some logic from Text.draw
line_cache.screen_line_starting_pos = {1}
local x = State.left
local pos = 1
for _, f in ipairs(line_cache.fragments) do
local frag, frag_text = f.data, f.text
-- render fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
x = State.left
table.insert(line_cache.screen_line_starting_pos, pos)
end
x = x + frag_width
local frag_len = utf8.len(frag)
pos = pos + frag_len
end
Text.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 boundaries
end
end
if #frag > 0 then
end
x = x + frag_width
end
end
if App.mouse_down(1) then return end
if 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)
end
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
end
end
-- Don't handle any keys here that would trigger love.textinput above.
if chord == 'return' then
Text.insert_return(State)
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
end
schedule_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 then
elseif 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)
end
elseif chord == 'backspace' then
if byte_start then
if byte_end then
else
end
end
State.cursor1.line = State.cursor1.line-1
end
top2 = Text.previous_screen_line(State, top2, State.left, State.right)
end
elseif chord == 'delete' then
end
local 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) then
if byte_start then
if byte_end then
else
end
end
table.remove(State.lines, State.cursor1.line+1)
end
--== shortcuts that move the cursor
elseif chord == 'left' then
elseif chord == 'right' then
elseif chord == 'S-left' then
elseif chord == 'S-right' then
Text.right(State)
Text.left(State)
-- C- hotkeys reserved for drawings, so we'll use M-
elseif chord == 'M-left' then
elseif chord == 'M-right' then
elseif chord == 'M-S-left' then
elseif chord == 'M-S-right' then
Text.word_right(State)
Text.word_left(State)
elseif chord == 'home' then
elseif chord == 'end' then
elseif chord == 'S-home' then
elseif chord == 'S-end' then
elseif chord == 'up' then
elseif chord == 'down' then
elseif chord == 'S-up' then
elseif chord == 'S-down' then
Text.down(State)
elseif chord == 'pageup' then
elseif chord == 'pagedown' then
elseif chord == 'S-pageup' then
elseif chord == 'S-pagedown' then
Text.pagedown(State)
end
end
function 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 line
local 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 = nil
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1 = {line=State.cursor1.line+1, pos=1}
else
-- disable enter when cursor is on the B side
end
end
-- 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_height
if State.screen_top1.line == 1 and State.screen_top1.pos and State.screen_top1.pos == 1 then break end
local y = App.screen.height - State.line_height
while y >= State.top do
local 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 line
if 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) - 1
end
else
-- move up one screen line in current line
assert(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)
end
end
--? 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 line
if State.cursor1.line > 1 then
State.cursor1.line = State.cursor1.line-1
State.cursor1.posB = nil
local prev_line_cache = State.line_cache[State.cursor1.line]
local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos]
local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos)
local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset)
State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
end
elseif screen_line_indexB == 2 then
-- all-B screen-line to potentially A+B screen-line
local xA = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
if State.cursor_x < xA then
State.cursor1.posB = nil
local new_screen_line_starting_pos = line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]
local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
else
local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x-xA, State.left) - 1
end
else
assert(screen_line_indexB > 2)
-- all-B screen-line to all-B screen-line
local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
end
Text.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 line
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
local 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 then
Text.upA(State)
else
Text.upB(State)
end
end
function 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) then
State.screen_top1 = new_top1
else
end
--? 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)
end
else
-- move down one screen line (B side) in current line
local scroll_down = false
if Text.le1(State.screen_bottom1, State.cursor1) then
scroll_down = true
end
local cursor_line = State.lines[State.cursor1.line]
local cursor_line_cache = State.line_cache[State.cursor1.line]
local cursor2 = Text.to2(State, State.cursor1)
assert(cursor2.screen_lineB < #cursor_line_cache.screen_line_starting_posB)
local new_screen_line_starting_byte_offsetB = Text.offset(cursor_line.dataB, new_screen_line_starting_posB)
local s = string.sub(cursor_line.dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
if scroll_down then
Text.snap_cursor_to_bottom_of_screen(State)
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]
end
end
end
end
end
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
while true do
break
end
end
end
while true do
break
end
Text.right_without_scroll(State)
end
end
function Text.skip_non_whitespace_rightB(State)
while true do
if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
break
end
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%s') then
break
end
Text.right_without_scroll(State)
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
break
end
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%S') then
break
end
Text.right_without_scroll(State)
if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
end
end
function Text.skip_non_whitespace_rightA(State)
while true do
while true do
break
end
break
end
end
end
function Text.skip_whitespace_rightB(State)
Text.right_without_scroll(State)
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then
if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
function Text.skip_non_whitespace_leftB(State)
while true do
if State.cursor1.posB == 1 then
break
end
assert(State.cursor1.posB > 1)
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%s') then
break
end
Text.left(State)
end
end
function Text.skip_whitespace_right(State)
if State.cursor1.pos then
Text.skip_whitespace_rightA(State)
else
Text.skip_whitespace_rightB(State)
end
end
function Text.skip_non_whitespace_right(State)
if State.cursor1.pos then
Text.skip_non_whitespace_rightA(State)
else
Text.skip_non_whitespace_rightB(State)
end
end
function Text.skip_whitespace_rightA(State)
Text.left(State)
break
end
break
end
end
while true do
if State.cursor1.pos == 1 then
break
end
assert(State.cursor1.pos > 1)
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
Text.left(State)
end
end
function Text.skip_non_whitespace_leftA(State)
while true do
if State.cursor1.posB == 1 then
break
end
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%S') then
break
end
end
function Text.skip_whitespace_leftB(State)
Text.left(State)
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then
if State.cursor1.pos == 1 then
function Text.word_left(State)
-- we can cross the fold, so check side A/B one level down
Text.skip_whitespace_left(State)
Text.left(State)
Text.skip_non_whitespace_left(State)
end
function Text.word_right(State)
-- we can cross the fold, so check side A/B one level down
Text.skip_whitespace_right(State)
Text.right(State)
Text.skip_non_whitespace_right(State)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.skip_whitespace_left(State)
if State.cursor1.pos then
Text.skip_whitespace_leftA(State)
else
Text.skip_whitespace_leftB(State)
end
end
function Text.skip_non_whitespace_left(State)
if State.cursor1.pos then
Text.skip_non_whitespace_leftA(State)
else
Text.skip_non_whitespace_leftB(State)
end
end
function Text.skip_whitespace_leftA(State)
function Text.end_of_line(State)
if State.cursor1.pos then
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
else
State.cursor1.posB = utf8.len(State.lines[State.cursor1.line].dataB) + 1
end
function Text.start_of_line(State)
if Text.lt1(State.cursor1, State.screen_top1) then
State.screen_top1 = {line=State.cursor1.line, pos=1} -- copy
if State.cursor1.pos then
State.cursor1.pos = 1
else
State.cursor1.posB = 1
end
--? 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) - 1
if scroll_down then
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)
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 line
local scroll_down = false
local scroll_down = Text.le1(State.screen_bottom1, State.cursor1)
elseif State.cursor1.pos then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
end
assert(false)
end
local screen_line
else
end
local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
return App.width(screen_line_text)
end
return i
end
end
function Text.screen_line_index(screen_line_starting_pos, pos)
for i = #screen_line_starting_pos,1,-1 do
if screen_line_starting_pos[i] <= pos then
function Text.screen_line_widthB(State, line_index, i)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
local start_posB = line_cache.screen_line_starting_posB[i]
local start_offsetB = Text.offset(line.dataB, start_posB)
local screen_line
if i < #line_cache.screen_line_starting_posB then
--? print('non-final', i)
local past_end_posB = line_cache.screen_line_starting_posB[i+1]
local past_end_offsetB = Text.offset(line.dataB, past_end_posB)
else
end
local 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)
end
function Text.screen_line_indexB(screen_line_starting_posB, posB)
if posB == nil then
return 0
end
assert(screen_line_starting_posB)
for i = #screen_line_starting_posB,1,-1 do
if screen_line_starting_posB[i] <= posB then
return i
end
end
end
-- convert x pixel coordinate to pos
-- oblivious to wrapping
function Text.nearest_cursor_pos(line, x, left)
if x < left then
-- result: 1 to len+1
screen_line = string.sub(line.data, start_pos)
if i < #line_cache.screen_line_starting_pos then
local 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 1
end
if x > max_x then
end
--? print('-- nearest', x)
while true do
end
if currxmin <= x and x < currxmax then
end
end
if currxmin > x then
else
end
end
end
local len = utf8.len(line)
if x > max_x then
return len+1
end
while true do
local curr = math.floor((left+right)/2)
if currxmin <= x and x < currxmax then
return curr
end
if left >= right-1 then
return left
end
if currxmin > x then
right = curr
else
left = curr
end
end
assert(false)
end
result.screen_line = i
break
end
end
assert(result.screen_pos)
return result
end
end
return result
end
function Text.lt1(a, b)
if a.line < b.line then
return true
end
if a.line > b.line then
return false
end
return true
end
return false
end
end
function Text.offset(s, pos1)
if pos1 == 1 then return 1 end
local result = utf8.offset(s, pos1)
if result == nil then
print(pos1, #s, s)
end
assert(result)
return result
end
else
end
end
if pos == State.screen_top1.pos then
break
end
-- make sure screen top is at start of a screen line
if State.screen_top1.pos - prev < pos - State.screen_top1.pos then
State.screen_top1.pos = prev
else
end
break
end
end
-- make sure cursor is on screen
--? print('too low')
--? print('tweak')
end
end
end
end
end
end
function Text.clear_screen_line_cache(State, line_index)
State.line_cache[line_index].fragments = nil
State.line_cache[line_index].screen_line_starting_pos = nil
State.line_cache[line_index].screen_line_starting_posB = nil
end
function trim(s)
return s:gsub('^%s+', ''):gsub('%s+$', '')
end
function ltrim(s)
return s:gsub('^%s+', '')
end
function rtrim(s)
return s:gsub('%s+$', '')
State.line_cache[line_index].fragmentsB = nil
--? print('clearing fragments')
for i=1,#State.lines do
State.line_cache[i] = {}
State.line_cache = {}
function Text.redraw_all(State)
end
-- slightly expensive since it redraws the screen
App.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 == nil
function 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) then
if Text.lt1(State.cursor1, State.screen_top1) then
State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos}
elseif State.cursor1.line >= State.screen_bottom1.line then
State.screen_top1.pos = pos
local prev = line_cache.screen_line_starting_pos[i-1]
if pos > State.screen_top1.pos then
-- resize helper
if State.screen_top1.pos == 1 then return end
local line = State.lines[State.screen_top1.line]
for i=2,#line_cache.screen_line_starting_pos do
local 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-line
return {line=loc2.line, screen_line=#State.line_cache[loc2.line].screen_line_starting_pos, screen_pos=1}
end
end
if loc2.screen_lineB > 2 then -- first screen line of B side overlaps with A side
return {line=loc2.line, screen_lineB=loc2.screen_lineB-1, screen_posB=1}
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) then
return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
end
-- try to switch to B
local prev_line_cache = State.line_cache[loc2.line-1]
local x = Margin_left + Text.screen_line_width(State, loc2.line-1, #prev_line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, loc2.line-1, x)
local screen_line_starting_posB = State.line_cache[loc2.line-1].screen_line_starting_posB
if #screen_line_starting_posB > 1 then
return {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 A
return {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 loc2
if loc2.screen_pos then
return Text.previous_screen_lineA(State, loc2)
else
return Text.previous_screen_lineB(State, loc2)
end
end
function Text.previous_screen_lineA(State, loc2)
if a.pos then
return a.pos < b.pos
else
return a.posB < b.posB
end
end
function Text.le1(a, b)
return eq(a, b) or Text.lt1(a, b)
if not a.pos and b.pos then
-- A side < B side
if a.pos and not b.pos then
end
function Text.to1B(State, loc2)
local result = {line=loc2.line, posB=loc2.screen_posB}
if loc2.screen_lineB > 1 then
result.posB = State.line_cache[loc2.line].screen_line_starting_posB[loc2.screen_lineB] + loc2.screen_posB - 1
end
return result
function Text.to1(State, loc2)
if loc2.screen_pos then
return Text.to1A(State, loc2)
else
return Text.to1B(State, loc2)
end
end
function Text.to1A(State, loc2)
local result = {line=loc2.line, pos=loc2.screen_pos}
if loc2.screen_line > 1 then
result.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1
return result
end
function Text.to2B(State, loc1)
local result = {line=loc1.line}
if sposB <= loc1.posB then
result.screen_lineB = i
result.screen_posB = loc1.posB - sposB + 1
break
end
end
assert(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_padding
Text.populate_screen_line_starting_posB(State, loc1.line, x)
for i=#line_cache.screen_line_starting_posB,1,-1 do
local sposB = line_cache.screen_line_starting_posB[i]
result.screen_pos = loc1.pos - spos + 1
Text.populate_screen_line_starting_pos(State, loc1.line)
if spos <= loc1.pos then
for i=#line_cache.screen_line_starting_pos,1,-1 do
local 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)
end
function Text.to2(State, loc1)
local result = {line=loc1.line}
local line_cache = State.line_cache[loc1.line]
if loc1.pos then
return Text.to2A(State, loc1)
else
return Text.to2B(State, loc1)
end
end
function Text.to2A(State, loc1)
local result = {line=loc1.line}
local line_cache = State.line_cache[loc1.line]
end
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)
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+1
local 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 margin
function Text.nearest_pos_less_than(line, x)
--? print('', '-- nearest_pos_less_than', line, x)
-- result: 0 to len+1
assert(false)
leftpos = curr
rightpos = curr
if leftpos >= rightpos-1 then
return rightpos
if x-currxmin < currxmax-x then
return curr
else
return curr+1
end
--? 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 then
return leftpos
local leftpos, rightpos = 1, len+1
return len+1
local 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) or
return screen_lines[#screen_lines] <= State.cursor1.pos
end
if State.cursor1.pos then
-- ignore B side
return screen_lines[#screen_lines] <= State.cursor1.pos
end
assert(State.cursor1.posB)
local line_cache = State.line_cache[State.cursor1.line]
local x = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, State.cursor1.line, x)
local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_posB
return screen_lines[#screen_lines] <= State.cursor1.posB
line.dataB == nil then
end
--? print('scroll up')
end
end
while true do
end
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)
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
Foo = 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)
end
if y < line_cache.starty then return false end
local num_screen_lines = 0
if line_cache.startpos then
Text.populate_screen_line_starting_pos(State, line_index)
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1
end
Text.populate_screen_line_starting_posB(State, line_index, x)
--? print('B:', x, #line_cache.screen_line_starting_posB)
if line_cache.startposB then
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB) -- no +1; first screen line of B side overlaps with A side
else
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, 1) -- no +1; first screen line of B side overlaps with A side
end
local 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) then
end
--? print('#screenlines after B', num_screen_lines)
return y < line_cache.starty + State.line_height*num_screen_lines
end
assert(my >= line_cache.starty)
-- duplicate some logic from Text.draw
local nexty = y + State.line_height
if my < nexty then
-- On all wrapped screen lines but the final one, clicks past end of
-- line position cursor on final character of screen line.
-- (The final screen line positions past end of screen line as always.)
--? print('past end of non-final line; return')
return nil, line_cache.screen_line_starting_posB[screen_line_indexB+1]-1
end
end
y = nexty
end
assert(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) - 1
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('aa', mx, State.left, Text.screen_line_widthB(State, line_index, screen_line_indexB))
local y = line_cache.starty
if line_cache.startpos then
local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
--? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
local nexty = y + State.line_height
if my < nexty then
-- On all wrapped screen lines but the final one, clicks past end of
-- line position cursor on final character of screen line.
-- (The final screen line positions past end of screen line as always.)
if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
--? print('past end of non-final line; return')
return line_cache.screen_line_starting_pos[screen_line_index+1]-1
end
local s = string.sub(line.data, screen_line_starting_byte_offset)
--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
-- no B side
end
local screen_line_posB = Text.nearest_cursor_pos(line.dataB, mx, --[[no left margin]] 0)
return nil, screen_line_posB
end
y = nexty
end
end
-- look in screen lines composed entirely of the B side
for screen_line_indexB = start_screen_line_indexB,#line_cache.screen_line_starting_posB do
local screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB]
local screen_line_starting_byte_offsetB = Text.offset(line.dataB, screen_line_starting_posB)
--? print('iter2', y, screen_line_indexB, screen_line_starting_posB, string.sub(line.dataB, screen_line_starting_byte_offsetB))
assert(State.expanded or line.expanded)
local start_screen_line_indexB
if line_cache.startposB then
start_screen_line_indexB = Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB)
else
start_screen_line_indexB = 2 -- skip the first line of side B, which we checked above
end
mx = mx - max_xA - AB_padding
return screen_line_starting_pos + screen_line_posA - 1
end
if not State.expanded and not line.expanded then
-- B side is not expanded
return screen_line_starting_pos + screen_line_posA - 1
end
local lenA = utf8.len(s)
if screen_line_posA < lenA then
-- mx is within A side
return screen_line_starting_pos + screen_line_posA - 1
end
local max_xA = State.left+Text.x(s, lenA+1)
if mx < max_xA + AB_padding then
-- mx is in the space between A and B side
return screen_line_starting_pos + screen_line_posA - 1
if line.dataB == nil then
local 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, posB
function 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 -> dataB
function 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 page
local line_cache = State.line_cache[line_index]
y = y - h
top2 = Text.previous_screen_line(State, top2)
if top2.line == 1 and top2.screen_line == 1 then break end
local h = State.line_height
if y - h < State.top then
break
--? 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.cursor1
function 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 line
if top2.screen_pos then
top2.screen_pos = 1
else
assert(top2.screen_posB)
top2.screen_posB = 1
end
--? 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 then
function 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
return spos,i
end
end
assert(false)
end
return sposB,i
function 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_padding
Text.populate_screen_line_starting_posB(State, loc1.line, x)
for i=#line_cache.screen_line_starting_posB,1,-1 do
local sposB = line_cache.screen_line_starting_posB[i]
if sposB <= loc1.posB then
Text.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 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
end
end
function 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)
end
end
end
end
Text.right_without_scroll(State)
Text.snap_cursor_to_bottom_of_screen(State)
if Text.cursor_out_of_screen(State) then
end
end
end
function Text.right_without_scroll(State)
State.cursor1.pos = State.cursor1.pos+1
elseif State.cursor1.line <= #State.lines-1 then
-- overflow back into A side
State.cursor1 = {line=State.cursor1.line+1, pos=1}
elseif State.cursor1.line <= #State.lines-1 then
State.cursor1 = {line=State.cursor1.line+1, pos=1}
end
end
function Text.right_without_scrollB(State)
if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
State.cursor1.posB = State.cursor1.posB+1
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
if State.cursor1.pos then
Text.right_without_scrollA(State)
else
Text.right_without_scrollB(State)
end
end
function Text.right_without_scrollA(State)
function Text.right(State)
if Text.lt1(State.cursor1, State.screen_top1) then
State.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 then
State.cursor1.pos = State.cursor1.pos-1
elseif State.cursor1.line > 1 then
State.cursor1 = {
line = State.cursor1.line-1,
}
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
end
function Text.leftB(State)
if State.cursor1.posB > 1 then
State.cursor1.posB = State.cursor1.posB-1
else
-- overflow back into A side
State.cursor1.posB = nil
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
pos = utf8.len(State.lines[State.cursor1.line-1].data) + 1,
function Text.left(State)
if State.cursor1.pos then
Text.leftA(State)
else
Text.leftB(State)
end
end
function 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 lines
State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
-- delete side B on first line
State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB
elseif State.cursor1.posB then
if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB+1)
if byte_start then
if byte_end then
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
else
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
end
-- no change to State.cursor1.pos
end
else
-- refuse to delete past end of side B
end
-- no change to State.cursor1.pos
State.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 before
else
before = 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) then
assert(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 leaks
local top2 = Text.to2(State, State.screen_top1)
if State.screen_top1.line > #State.lines then
Text.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) then
elseif State.cursor1.line > 1 then
before = snapshot(State, State.cursor1.line-1, State.cursor1.line)
-- join lines
table.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)+1
State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
State.cursor1.pos = State.cursor1.pos-1
end
elseif State.cursor1.posB then
if State.cursor1.posB > 1 then
before = snapshot(State, State.cursor1.line)
local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1)
local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
if byte_start then
if byte_end then
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
else
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
end
State.cursor1.posB = State.cursor1.posB-1
end
else
-- refuse to delete past beginning of side B
State.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 before
before = 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 then
schedule_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 then
Text.populate_screen_line_starting_pos(State, State.cursor1.line)
local before = snapshot(State, State.cursor1.line)
local before_line = State.cursor1.line
local before = snapshot(State, before_line)
--== shortcuts that mutate text
function Text.keychord_pressed(State, chord)
--? print('chord', chord)
function Text.insert_at_cursor(State, t)
if State.cursor1.pos then
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.pos = State.cursor1.pos+1
else
assert(State.cursor1.posB)
local byte_offset = Text.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].dataB, byte_offset)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.posB = State.cursor1.posB+1
end
Text.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 then
local 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 line
local 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 narrow
if (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*') do
local 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 narrow
local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos
--? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
local frag1 = string.sub(frag, 1, boffset-1)
local frag1_text = App.newText(love.graphics.getFont(), frag1)
local frag1_width = App.width(frag1_text)
--? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
assert(x + frag1_width <= State.right)
frag = string.sub(frag, boffset)
frag_text = App.newText(love.graphics.getFont(), frag)
frag_width = App.width(frag_text)
end
x = State.left -- new line
end
if #frag > 0 then
--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
end
x = x + frag_width
end
end
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
return
end
-- duplicate some logic from Text.draw
local pos = 1
local frag, frag_text = f.data, f.text
-- render fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
x = State.left
end
x = x + frag_width
local frag_len = utf8.len(frag)
pos = pos + frag_len
end
end
-- try to wrap at word boundaries
for 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 then
return
end
function Text.compute_fragmentsB(State, line_index, x)
table.insert(line_cache.screen_line_starting_posB, pos)
for _, f in ipairs(line_cache.fragmentsB) do
line_cache.screen_line_starting_posB = {1}
Text.compute_fragmentsB(State, line_index, x)
if line_cache.screen_line_starting_posB then
function 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 then
return
end
line_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_width
App.screen.draw(frag_text, x,y)
-- render cursor if necessary
if State.search_term then
love.graphics.print(State.search_term, x+lo_px,y)
end
Text.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y)
App.color(Fold_color)
elseif Focus == 'edit' then
App.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 then
local 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 then
if pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB then
x = State.left
return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
local frag_len = utf8.len(frag)
-- render nothing
--? print('skipping', frag)
else
if x + frag_width > State.right then
assert(x > State.left) -- no overfull lines
-- render fragment
local frag_width = App.width(frag_text)
--? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
if pos < startpos then
Text.compute_fragments(State, line_index)
local pos = 1
initialize_color()
return y, nil, screen_line_starting_pos
end
-- check for B side
assert(y)
assert(screen_line_starting_pos)
--? if line_index == 8 then print('return 1') end
return y, screen_line_starting_pos
end
if not State.expanded and not line.expanded then
assert(y)
assert(screen_line_starting_pos)
App.color(Fold_background_color)
end,
onpress1 = function()
line.expanded = true
end,
})
return y, screen_line_starting_pos
end
-- draw B side
App.color(Fold_color)
if overflows_screen then
end
--? if line_index == 8 then print('a') end
--? if line_index == 8 then print('b') end
if State.search_term == nil then
if line_index == State.cursor1.line and State.cursor1.posB == pos then
Text.draw_cursor(State, x, y)
--? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) end
if Focus == 'edit' and State.cursor1.posB then
return y, nil, screen_line_starting_pos
if startposB then
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB)
else
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x+AB_padding,y, 1)
end
--? if 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') end
love.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') end
button(State, 'expand', {x=x+AB_padding, y=y+2, w=App.width(State.em), h=State.line_height-4, color={1,1,1},
icon = function(button_params)
--? if line_index == 8 then print('checking for B side') end
if line.dataB == nil then
local line = State.lines[line_index]
line_cache.starty = y
line_cache.startpos = startpos
-- draw A side
end
end
else
x = State.left
local overflows_screen, x, pos, screen_line_starting_pos
if startpos then
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_line(State, line_index, State.left, y, startpos)
if overflows_screen then
return y, screen_line_starting_pos
end
if State.search_term == nil then
if line_index == State.cursor1.line and State.cursor1.pos == pos then
Text.draw_cursor(State, x, y)
end
if Focus == 'edit' and State.cursor1.pos then
line_cache.startposB = startposB
local 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 drawn
function 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 file
if infile then infile:close() end
end
function load_from_file(infile)
local result = {}
if infile then
local infile_next_line = infile:lines() -- works with both Lua files and LÖVE Files (https://www.love2d.org/wiki/File)
while true do
local line = infile_next_line()
if line == nil then break end
end
end
if #result == 0 then
end
return result
end
if outfile == nil then
end
end
outfile:close()
end
end
-- for tests
function load_array(a)
local result = {}
local next_line = ipairs(a)
while true do
i,line = next_line(a, i)
if i == nil then break end
end
if #result == 0 then
end
return result
table.insert(result, {data=''})
local line_info = {}
else
line_info.data = line
end
table.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) do
outfile:write(line.data)
outfile:write('\n')
outfile:write(line.data)
outfile:write(Fold)
end
outfile:write('\n')
outfile:write(line.dataB)
if line.dataB and #line.dataB > 0 then
error('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)
else
line_info.data = line
end
if 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 then
infile:close()
return true
else
return false
end
end
Fold = '\x1e' -- ASCII RS (record separator)
-- some constants people might like to tweak
Text_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 over
Highlight_color = {r=0.7, g=0.7, b=0.9} -- selected text
Margin_top = 15
Margin_left = 25
Margin_right = 25
edit = {}
-- run in both tests and a real run
local 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 side
lines = {{data='', dataB=nil, expanded=nil}}, -- array of lines
font_height = font_height,
line_height = line_height,
em = App.newText(love.graphics.getFont(), 'm'), -- widest possible character width
filename = love.filesystem.getUserDirectory()..'/lines.txt',
next_save = nil,
--? print('== draw')
--? print('draw:', y, line_index, line)
end
y = y + State.line_height
--? print('=> y', y)
end
end
end
if State.next_save and State.next_save < App.getTime() then
State.next_save = nil
end
end
end
end
-- make sure to save before quitting
end
end
end
end
end
end
else
end
end
if chord == 'escape' then
elseif chord == 'return' then
elseif chord == 'backspace' then
elseif chord == 'down' then
elseif chord == 'up' then
end
return
elseif chord == 'C-f' then
elseif chord == 'C-=' then
elseif chord == 'C--' then
elseif chord == 'C-0' then
elseif chord == 'C-z' then
if event then
local src = event.before
schedule_save(State)
end
elseif chord == 'C-y' then
if event then
local src = event.after
schedule_save(State)
end
-- clipboard
elseif chord == 'C-c' then
if s then
App.setClipboardText(s)
end
elseif chord == 'C-x' then
local s = Text.cut_selection(State, State.left, State.right)
if s then
App.setClipboardText(s)
end
elseif 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) do
local c = utf8.char(code)
if c == '\n' then
else
end
end
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
end
else
end
end
end
-- not all keys are textinput
function edit.run_after_keychord(State, chord)
edit.keychord_pressed(State, chord)
edit.key_released(State, chord)
App.screen.contents = {}
edit.draw(State)
end
App.screen.contents = {}
edit.draw(State)
end
App.screen.contents = {}
edit.draw(State)
end
App.screen.contents = {}
edit.draw(State)
end
function 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 keys
function 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 tests
Test_margin_left = 25
function edit.initialize_test_state()
-- if you change these values, tests will start failing
return edit.initialize_state(
15, -- top margin
Test_margin_left,
App.screen.width, -- right margin = 0
14, -- font height assuming default LÖVE font
15) -- line height
end
function edit.update_font_settings(State, font_height)
State.font_height = font_height
love.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')
end
Text_cache = {}
Text.keychord_pressed(State, chord)
end
end
function edit.eradicate_locations_after_the_fold(State)
-- eradicate side B from any locations we track
if State.cursor1.posB then
State.cursor1.posB = nil
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data)
State.cursor1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
end
if State.screen_top1.posB then
State.screen_top1.posB = nil
State.screen_top1.pos = utf8.len(State.lines[State.screen_top1.line].data)
State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.screen_top1)
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
-- dispatch to text
if Text.cursor_out_of_screen(State) then
Text.insert_at_cursor(State, c)
Text.insert_return(State)
local before_line = State.cursor1.line
local before = snapshot(State, before_line)
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
schedule_save(State)
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
local 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 leaks
Text.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 scroll
State.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 leaks
Text.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 scroll
Text.redraw_all(State)
-- undo
edit.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 text
elseif chord == 'C-b' then
State.expanded = not State.expanded
Text.redraw_all(State)
if not State.expanded then
for _,line in ipairs(State.lines) do
line.expanded = nil
end
end
elseif chord == 'C-d' then
if State.cursor1.posB == nil then
if State.lines[State.cursor1.line].dataB == nil then
State.lines[State.cursor1.line].dataB = ''
end
State.lines[State.cursor1.line].expanded = true
State.cursor1.pos = nil
State.cursor1.posB = 1
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
end
end
schedule_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)
-- zoom
State.search_backup = {
cursor={line=State.cursor1.line, pos=State.cursor1.pos},
screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos},
}
State.search_backup = {
}
cursor={line=State.cursor1.line, pos=State.cursor1.pos, 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 then
State.cursor1.pos = State.cursor1.pos+1
else
State.cursor1.posB = State.cursor1.posB+1
end
local len = utf8.len(State.search_term)
local byte_offset = Text.offset(State.search_term, len)
State.search_term = string.sub(State.search_term, 1, byte_offset-1)
State.search_text = nil
State.search_term = nil
State.search_text = nil
State.search_backup = nil
State.search_term = nil
State.search_text = nil
State.cursor1 = State.search_backup.cursor
State.screen_top1 = State.search_backup.screen_top
State.search_backup = nil
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
if State.search_term then
function edit.keychord_pressed(State, chord, key)
schedule_save(State)
Text.textinput(State, t)
function edit.textinput(State, t)
if State.search_term then
State.search_term = State.search_term..t
State.search_text = nil
Text.search_next(State)
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
function edit.mouse_released(State, x,y, mouse_button)
for line_index,line in ipairs(State.lines) do
if Text.in_line(State, line_index, x,y) then
break
local 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-circuit
return
end
if 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 then
save_to_disk(State)
function edit.quit(State)
function schedule_save(State)
if State.next_save == nil then
State.next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you did
save_to_disk(State)
function edit.update(State, dt)
if State.search_term then
Text.draw_search_bar(State)
end
y, 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 then
startpos = State.screen_top1.pos
if State.screen_top1.pos then
startpos = State.screen_top1.pos
else
startpos, startposB = nil, State.screen_top1.posB
end
local startpos = 1
local startpos, startposB = 1, nil
State.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 do
local line = State.lines[line_index]
App.color(Text_color)
assert(#State.lines == #State.line_cache)
local y = State.top
if not Text.le1(State.screen_top1, State.cursor1) then
assert(false)
end
State.cursor_x = nil
State.cursor_y = nil
print(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 = {}
-- search
search_term = nil,
search_text = nil,
search_backup = nil, -- stuff to restore when cancelling search
}
return result
end -- App.initialize_state
-- undo
history = {},
next_history = 1,
top = top,
left = left,
right = right,
width = right-left,
-- cursor coordinates in pixels
cursor_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 screen
cursor1 = {line=1, pos=1, posB=nil}, -- position of cursor
screen_bottom1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at bottom of screen
-- 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 screen
Fold_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' then
Focus = 'edit'
end
edit.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' then
Focus = 'log_browser'
end
log_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 scroll
end
end
function source.mouse_released(x,y, mouse_button)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Focus == 'edit' then
return edit.mouse_released(Editor_state, x,y, mouse_button)
else
return log_browser.mouse_released(Log_browser_state, x,y, mouse_button)
end
end
function source.textinput(t)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Focus == 'edit' then
return edit.textinput(Editor_state, t)
else
return log_browser.textinput(Log_browser_state, t)
end
end
function source.keychord_pressed(chord, key)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
--? print('source keychord')
if Show_file_navigator then
keychord_pressed_on_file_navigator(chord, key)
return
end
if chord == 'C-l' then
--? print('C-l')
Show_log_browser_side = not Show_log_browser_side
if Show_log_browser_side then
App.screen.width = Log_browser_state.right + Margin_right
else
App.screen.width = Editor_state.right + Margin_right
end
--? 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 this
source.set_window_position_from_settings(Settings.source)
return
end
if chord == 'C-g' then
Show_file_navigator = true
File_navigation.index = 1
return
end
if Focus == 'edit' then
return edit.keychord_pressed(Editor_state, chord, key)
else
return log_browser.keychord_pressed(Log_browser_state, chord, key)
end
end
function source.key_released(key, scancode)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Focus == 'edit' then
return edit.key_released(Editor_state, key, scancode)
else
return log_browser.keychord_pressed(Log_browser_state, chordkey, scancode)
end
end
-- use this sparingly
function to_text(s)
if Text_cache[s] == nil then
Text_cache[s] = App.newText(love.graphics.getFont(), s)
end
return 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 changes
Text_cache = {}
-- blinking cursor
Cursor_time = 0
end
-- called only for real run
function source.initialize()
love.keyboard.setTextInput(true) -- bring up keyboard on touch screen
love.keyboard.setKeyRepeat(true)
love.graphics.setBackgroundColor(1,1,1)
if Settings and Settings.source then
source.load_settings()
else
source.initialize_default_settings()
end
source.initialize_edit_side{'run.lua'}
source.initialize_log_browser_side()
Menu_status_bar_height = 5 + Editor_state.line_height + 5
Editor_state.top = Editor_state.top + Menu_status_bar_height
Log_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 yet
else
assert(false, 'unknown focus "'..Focus..'"')
end
add_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')
end
function add_hotkey_to_menu(s)
if Text_cache[s] == nil then
Text_cache[s] = App.newText(love.graphics.getFont(), s)
end
local width = App.width(Text_cache[s])
if Menu_cursor + width > App.screen.width - 5 then
return
end
App.color(Menu_command_color)
App.screen.draw(Text_cache[s], Menu_cursor,5)
Menu_cursor = Menu_cursor + width + 30
end
function source.draw_file_navigator()
for i,file in ipairs(File_navigation.candidates) do
if file == 'source' then
App.color(Menu_border_color)
love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)
end
add_file_to_menu(file, i == File_navigation.index)
end
end
function add_file_to_menu(s, cursor_highlight)
if Text_cache[s] == nil then
Text_cache[s] = App.newText(love.graphics.getFont(), s)
end
local width = App.width(Text_cache[s])
if Menu_cursor + width > App.screen.width - 5 then
return
end
if cursor_highlight then
App.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)
end
App.color(Menu_command_color)
App.screen.draw(Text_cache[s], Menu_cursor,5)
Menu_cursor = Menu_cursor + width + 30
end
function keychord_pressed_on_file_navigator(chord, key)
if chord == 'escape' then
Show_file_navigator = false
elseif chord == 'return' then
local candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')
source.switch_to_file(candidate)
Show_file_navigator = false
elseif chord == 'left' then
if File_navigation.index > 1 then
File_navigation.index = File_navigation.index-1
end
elseif chord == 'right' then
if File_navigation.index < #File_navigation.candidates then
File_navigation.index = File_navigation.index+1
end
end
end
current_drawing_mode=Drawing_mode,
previous_drawing_mode=State.previous_drawing_mode,
if line.mode == 'text' then
table.insert(event.lines, {mode='text', data=line.data, dataB=line.dataB})
elseif line.mode == 'drawing' then
local 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={}})
else
print(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 operations
function record_undo_event(State, data)
State.history[State.next_history] = data
State.next_history = State.next_history+1
for i=State.next_history,#State.history do
State.history[i] = nil
end
end
function undo_event(State)
if State.next_history > 1 then
--? print('moving to history', State.next_history-1)
State.next_history = State.next_history-1
local result = State.history[State.next_history]
return result
end
end
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+1
return result
end
end
-- 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 then
e = s
end
assert(#State.lines > 0)
if s < 1 then s = 1 end
if s > #State.lines then s = #State.lines end
if e < 1 then e = 1 end
if e > #State.lines then e = #State.lines end
-- compare with App.initialize_globals
local event = {
screen_top=deepcopy(State.screen_top1),
selection=deepcopy(State.selection1),
cursor=deepcopy(State.cursor1),
end
return event
end
function 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
--? end
assert(from.start_line == to.start_line)
for i=from.end_line,from.start_line,-1 do
table.remove(lines, i)
end
assert(#to.lines == to.end_line-to.start_line+1)
for i=1,#to.lines do
table.insert(lines, to.start_line+i-1, to.lines[i])
end
end
function patch_placeholders(line_cache, from, to)
assert(from.start_line == to.start_line)
for i=from.end_line,from.start_line,-1 do
table.remove(line_cache, i)
end
assert(#to.lines == to.end_line-to.start_line+1)
for i=1,#to.lines do
table.insert(line_cache, to.start_line+i-1, {})
end
end
-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
function deepcopy(obj, seen)
if type(obj) ~= 'table' then return obj end
if seen and seen[obj] then return seen[obj] end
local s = seen or {}
local result = setmetatable({}, getmetatable(obj))
s[obj] = result
for k,v in pairs(obj) do
result[deepcopy(k, s)] = deepcopy(v, s)
end
return result
end
function minmax(a, b)
return math.min(a,b), math.max(a,b)
end
end
function 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 text
check_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')
end
function 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 drawing
edit.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')
end
function test_pagedown_skips_drawings()
io.write('\ntest_pagedown_skips_drawings')
-- some lines of text with a drawing intermixed
local drawing_width = 50
App.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 15
Text.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 80px
edit.draw(Editor_state)
local y = Editor_state.top
App.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 80px
edit.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_height
App.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 flows
function 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')
end
function 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 it
App.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 up
edit.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')
end
function 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.top
App.screen.check(y, 'a', 'F - test_insert_first_character/screen:1')
end
function test_press_ctrl()
io.write('\ntest_press_ctrl')
-- press ctrl while the cursor is on text
App.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')
end
function 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')
end
function 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')
end
function 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 line
end
function 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 line
edit.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')
end
function 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 words
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_previous_word')
end
function 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 word
edit.draw(Editor_state)
edit.run_after_keychord(Editor_state, 'M-left')
check_eq(Editor_state.cursor1.pos, 1, 'F - test_skip_to_previous_word')
end
function 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 word
edit.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')
end
function 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 word
edit.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')
end
function 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')
end
function 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')
end
function 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 words
edit.draw(Editor_state)
edit.run_after_keychord(Editor_state, 'M-right')
check_eq(Editor_state.cursor1.pos, 8, 'F - test_skip_to_next_word')
end
function 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 words
edit.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')
end
function 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 word
edit.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')
end
function 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')
end
function test_click_with_mouse()
io.write('\ntest_click_with_mouse')
-- display two lines with cursor on one of them
App.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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
-- cursor moves
check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse/cursor:line')
end
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 middle
App.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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left-4,Editor_state.top+5, 1)
-- cursor moves to start of line
check_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')
end
function 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 them
App.screen.init{width=100, height=80}
Editor_state = edit.initialize_test_state()
Editor_state.left = 50 -- occupy only right side of screen
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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
-- cursor moves
check_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')
end
function test_click_with_mouse_on_empty_line()
io.write('\ntest_click_with_mouse_on_empty_line')
-- display two lines with the first one empty
App.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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
-- cursor moves
check_eq(Editor_state.cursor1.line, 1, 'F - test_click_with_mouse_on_empty_line/cursor')
end
function 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.top
App.screen.check(y, 'abc', 'F - test_draw_text/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_draw_text/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_draw_text/screen:3')
end
function 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.top
App.screen.check(y, 'abc', 'F - test_draw_wrapping_text/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'de', 'F - test_draw_wrapping_text/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'fgh', 'F - test_draw_wrapping_text/screen:3')
end
function 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.top
App.screen.check(y, 'abc ', 'F - test_draw_word_wrapping_text/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def ', 'F - test_draw_word_wrapping_text/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_draw_word_wrapping_text/screen:3')
end
function test_click_with_mouse_on_wrapping_line()
io.write('\ntest_click_with_mouse_on_wrapping_line')
-- display two lines with cursor on one of them
App.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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
-- cursor moves
check_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')
end
function 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 them
App.screen.init{width=100, height=80}
Editor_state = edit.initialize_test_state()
Editor_state.left = 50 -- occupy only right side of screen
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 line
edit.draw(Editor_state)
edit.run_after_mouse_click(Editor_state, Editor_state.left+8,Editor_state.top+5, 1)
-- cursor moves
check_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')
end
function test_draw_text_wrapping_within_word()
-- arrange a screen line that needs to be split within a word
io.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.top
App.screen.check(y, 'abcd ', 'F - test_draw_text_wrapping_within_word/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'e fgh', 'F - test_draw_text_wrapping_within_word/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ijk', 'F - test_draw_text_wrapping_within_word/screen:3')
end
function test_draw_wrapping_text_containing_non_ascii()
-- draw a long line containing non-ASCII
io.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 apostrophe
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.top
App.screen.check(y, 'mad', 'F - test_draw_wrapping_text_containing_non_ascii/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'am I', 'F - test_draw_wrapping_text_containing_non_ascii/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, '’m a', 'F - test_draw_wrapping_text_containing_non_ascii/screen:3')
end
function test_click_on_wrapping_line()
io.write('\ntest_click_on_wrapping_line')
-- display a wrapping line
App.screen.init{width=75, height=80}
Editor_state = edit.initialize_test_state()
-- 12345678901234
Editor_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.top
App.screen.check(y, 'madam ', 'F - test_click_on_wrapping_line/baseline/screen:1')
y = y + Editor_state.line_height
App.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 line
edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
-- cursor moves to end of screen line
check_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')
end
function 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 line
App.screen.init{width=75, height=80}
Editor_state = edit.initialize_test_state()
-- 12345678901234
Editor_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.top
App.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 line
edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
-- cursor moves to end of screen line
check_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')
end
function test_click_past_end_of_wrapping_line()
io.write('\ntest_click_past_end_of_wrapping_line')
-- display a wrapping line
App.screen.init{width=75, height=80}
Editor_state = edit.initialize_test_state()
-- 12345678901234
Editor_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.top
App.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, "I'm ad", 'F - test_click_past_end_of_wrapping_line/baseline/screen:2')
y = y + Editor_state.line_height
App.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 it
edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
-- cursor moves to end of line
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-points
end
function 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-ASCII
App.screen.init{width=75, height=80}
Editor_state = edit.initialize_test_state()
-- 12345678901234
Editor_state.lines = load_array{'madam I’m adam'} -- notice the non-ASCII apostrophe
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.top
App.screen.check(y, 'madam ', 'F - test_click_past_end_of_wrapping_line_containing_non_ascii/baseline/screen:1')
y = y + Editor_state.line_height
App.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_height
App.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 it
edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
-- cursor moves to end of line
check_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-points
end
function 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 length
App.screen.init{width=160, height=80}
Editor_state = edit.initialize_test_state()
-- 0 1 2
-- 123456789012345678901
Editor_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.top
App.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 line
edit.run_after_mouse_click(Editor_state, App.screen.width-2,y-2, 1)
-- cursor moves to end of screen line
check_eq(Editor_state.cursor1.pos, 20, 'F - test_click_past_end_of_word_wrapping_line/cursor')
end
function 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.top
App.screen.check(y, 'abc', 'F - test_edit_wrapping_text/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'de', 'F - test_edit_wrapping_text/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'fg', 'F - test_edit_wrapping_text/screen:3')
end
function test_insert_newline()
io.write('\ntest_insert_newline')
-- display a few lines
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'}
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.top
App.screen.check(y, 'abc', 'F - test_insert_newline/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_insert_newline/baseline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_insert_newline/baseline/screen:3')
-- hitting the enter key splits the line
edit.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.top
App.screen.check(y, 'a', 'F - test_insert_newline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'bc', 'F - test_insert_newline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_insert_newline/screen:3')
end
function test_insert_newline_at_start_of_line()
io.write('\ntest_insert_newline_at_start_of_line')
-- display a line
App.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 line
edit.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')
end
function test_insert_from_clipboard()
io.write('\ntest_insert_from_clipboard')
-- display a few lines
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'}
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.top
App.screen.check(y, 'abc', 'F - test_insert_from_clipboard/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_insert_from_clipboard/baseline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_insert_from_clipboard/baseline/screen:3')
-- paste some text including a newline, check that new line is created
App.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.top
App.screen.check(y, 'axy', 'F - test_insert_from_clipboard/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'zbc', 'F - test_insert_from_clipboard/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_insert_from_clipboard/screen:3')
end
function 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_cache
edit.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')
end
function 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 displayed
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_pagedown/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_pagedown/baseline/screen:2')
-- after pagedown the bottom line becomes the top
edit.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.top
App.screen.check(y, 'def', 'F - test_pagedown/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_pagedown/screen:2')
end
end
function 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 line
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 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.top
App.screen.check(y, 'abc ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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.top
App.screen.check(y, 'ghi ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'mno ', 'F - test_pagedown_can_start_from_middle_of_long_wrapping_line/screen:3')
end
function test_pagedown_never_moves_up()
io.write('\ntest_pagedown_never_moves_up')
-- draw the final screen line of a wrapping line
App.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 change
edit.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')
end
function 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 displayed
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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 unchanged
y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_down_arrow_moves_cursor/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_down_arrow_moves_cursor/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_down_arrow_moves_cursor/screen:3')
end
function 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 line
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=3, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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.top
App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_down_arrow_scrolls_down_by_one_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_down_arrow_scrolls_down_by_one_line/screen:3')
end
function 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 line
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=3, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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.top
App.screen.check(y, 'def', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi ', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_down_arrow_scrolls_down_by_one_screen_line/screen:3')
end
function 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 line
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.top
App.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_height
App.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_height
App.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 line
edit.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.top
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_height
App.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_height
App.screen.check(y, 'kl', 'F - test_down_arrow_scrolls_down_by_one_screen_line_after_splitting_within_word/screen:3')
end
function 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.top
App.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_height
App.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_height
App.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 line
edit.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 up
edit.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.top
App.screen.check(y, 'ghij', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'kl', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'mno', 'F - test_page_down_followed_by_down_arrow_does_not_scroll_screen_up/screen:3')
end
function test_up_arrow_moves_cursor()
io.write('\ntest_up_arrow_moves_cursor')
-- display the first 3 lines with the cursor on the bottom line
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=3, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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 unchanged
y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_up_arrow_moves_cursor/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_up_arrow_moves_cursor/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_up_arrow_moves_cursor/screen:3')
end
function 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 2
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=2, pos=1}
Editor_state.screen_top1 = {line=2, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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.top
App.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_by_one_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_by_one_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_by_one_line/screen:3')
end
function 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 line
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=3, pos=6}
Editor_state.screen_top1 = {line=3, pos=5}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/baseline/screen:1')
y = y + Editor_state.line_height
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 line
edit.run_after_keychord(Editor_state, 'up')
y = Editor_state.top
App.screen.check(y, 'ghi ', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_by_one_screen_line/screen:2')
y = y + Editor_state.line_height
App.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')
end
function 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 line
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=1}
Editor_state.screen_top1 = {line=2, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_up_arrow_scrolls_up_to_final_screen_line/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.run_after_keychord(Editor_state, 'up')
y = Editor_state.top
App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_up_arrow_scrolls_up_to_final_screen_line/screen:2')
y = y + Editor_state.line_height
App.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')
end
function 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 screen
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=2, pos=1}
Editor_state.screen_top1 = {line=2, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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 line
y = y + Editor_state.line_height
App.screen.check(y, 'abc', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_up_arrow_scrolls_up_to_empty_line/screen:3')
end
function 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 displayed
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'def', 'F - test_pageup/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_pageup/baseline/screen:2')
-- after pageup the cursor goes to first line
edit.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.top
App.screen.check(y, 'abc', 'F - test_pageup/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_pageup/screen:2')
end
function 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 line
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=1}
Editor_state.screen_top1 = {line=2, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_by_screen_line/baseline/screen:2')
y = y + Editor_state.line_height
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 top
edit.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.top
App.screen.check(y, 'abc ', 'F - test_pageup_scrolls_up_by_screen_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_pageup_scrolls_up_by_screen_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_pageup_scrolls_up_by_screen_line/screen:3')
end
function 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.top
App.screen.check(y, 'jkl', 'F - test_pageup_scrolls_up_from_middle_screen_line/baseline/screen:2')
y = y + Editor_state.line_height
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 top
edit.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.top
App.screen.check(y, 'abc ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi ', 'F - test_pageup_scrolls_up_from_middle_screen_line/screen:3')
end
function test_enter_on_bottom_line_scrolls_down()
io.write('\ntest_enter_on_bottom_line_scrolls_down')
-- display a few lines with cursor on bottom line
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'}
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.top
App.screen.check(y, 'abc', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_enter_on_bottom_line_scrolls_down/baseline/screen:3')
-- after hitting the enter key the screen scrolls down
edit.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.top
App.screen.check(y, 'def', 'F - test_enter_on_bottom_line_scrolls_down/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'g', 'F - test_enter_on_bottom_line_scrolls_down/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'hi', 'F - test_enter_on_bottom_line_scrolls_down/screen:3')
end
function 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 screen
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'}
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.top
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 down
edit.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.top
App.screen.check(y, 'j', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'kl', 'F - test_enter_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:2')
end
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 screen
App.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 down
edit.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.top
App.screen.check(y, 'a', 'F - test_inserting_text_on_final_line_avoids_scrolling_down_when_not_at_bottom/screen:1')
end
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 line
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'}
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.top
App.screen.check(y, 'abc', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/baseline/screen:2')
y = y + Editor_state.line_height
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 down
edit.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.top
App.screen.check(y, 'def', 'F - test_typing_on_bottom_line_scrolls_down/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghij', 'F - test_typing_on_bottom_line_scrolls_down/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'kl', 'F - test_typing_on_bottom_line_scrolls_down/screen:3')
end
function 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 line
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.screen_top1 = {line=3, pos=5}
Editor_state.screen_bottom1 = {}
-- cursor is at top of screen
Editor_state.cursor1 = {line=3, pos=5}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/baseline/screen:1')
y = y + Editor_state.line_height
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 line
edit.run_after_keychord(Editor_state, 'left')
y = Editor_state.top
App.screen.check(y, 'ghi ', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_left_arrow_scrolls_up_in_wrapped_line/screen:2')
y = y + Editor_state.line_height
App.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')
end
function 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 line
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.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
-- cursor is at bottom right of screen
Editor_state.cursor1 = {line=3, pos=5}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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.top
App.screen.check(y, 'def', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi ', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_right_arrow_scrolls_down_in_wrapped_line/screen:3')
end
function 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 line
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.screen_top1 = {line=3, pos=5}
Editor_state.screen_bottom1 = {}
-- cursor is at top of screen
Editor_state.cursor1 = {line=3, pos=5}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/baseline/screen:1')
y = y + Editor_state.line_height
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 line
edit.run_after_keychord(Editor_state, 'home')
y = Editor_state.top
App.screen.check(y, 'ghi ', 'F - test_home_scrolls_up_in_wrapped_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_home_scrolls_up_in_wrapped_line/screen:2')
y = y + Editor_state.line_height
App.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')
end
function 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 line
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.screen_top1 = {line=1, pos=1}
Editor_state.screen_bottom1 = {}
-- cursor is at bottom right of screen
Editor_state.cursor1 = {line=3, pos=5}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'abc', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/baseline/screen:2')
y = y + Editor_state.line_height
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 line
edit.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.top
App.screen.check(y, 'def', 'F - test_end_scrolls_down_in_wrapped_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi ', 'F - test_end_scrolls_down_in_wrapped_line/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_end_scrolls_down_in_wrapped_line/screen:3')
end
function test_position_cursor_on_recently_edited_wrapping_line()
-- draw a line wrapping over 2 screen lines
io.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.top
App.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline1/screen:2')
y = y + Editor_state.line_height
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 lines
edit.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.top
App.screen.check(y, 'abc def ghi ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl mno pqr ', 'F - test_position_cursor_on_recently_edited_wrapping_line/baseline2/screen:2')
y = y + Editor_state.line_height
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
edit.run_after_mouse_press(Editor_state, Editor_state.left+8,Editor_state.top+Editor_state.line_height*2+5, 1)
-- cursor should move
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')
end
function test_backspace_can_scroll_up()
io.write('\ntest_backspace_can_scroll_up')
-- display the lines 2/3/4 with the cursor on line 2
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=2, pos=1}
Editor_state.screen_top1 = {line=2, pos=1}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'def', 'F - test_backspace_can_scroll_up/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/baseline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/baseline/screen:3')
-- after hitting backspace the screen scrolls up by one line
edit.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.top
App.screen.check(y, 'abcdef', 'F - test_backspace_can_scroll_up/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'ghi', 'F - test_backspace_can_scroll_up/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up/screen:3')
end
function 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 line
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=3, pos=5}
Editor_state.screen_top1 = {line=3, pos=5}
Editor_state.screen_bottom1 = {}
edit.draw(Editor_state)
local y = Editor_state.top
App.screen.check(y, 'jkl', 'F - test_backspace_can_scroll_up_screen_line/baseline/screen:1')
y = y + Editor_state.line_height
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 line
edit.run_after_keychord(Editor_state, 'backspace')
y = Editor_state.top
App.screen.check(y, 'ghij', 'F - test_backspace_can_scroll_up_screen_line/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'kl', 'F - test_backspace_can_scroll_up_screen_line/screen:2')
y = y + Editor_state.line_height
App.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')
end
function test_backspace_past_line_boundary()
io.write('\ntest_backspace_past_line_boundary')
-- position cursor at start of a (non-first) line
App.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 line
edit.run_after_keychord(Editor_state, 'backspace')
check_eq(Editor_state.lines[1].data, 'abcdef', "F - test_backspace_past_line_boundary")
end
function 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 character
edit.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.top
App.screen.check(y, 'abc', 'F - test_undo_insert_text/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'defg', 'F - test_undo_insert_text/baseline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'xyz', 'F - test_undo_insert_text/baseline/screen:3')
-- undo
edit.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.top
App.screen.check(y, 'abc', 'F - test_undo_insert_text/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_undo_insert_text/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'xyz', 'F - test_undo_insert_text/screen:3')
end
function 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 character
edit.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.top
App.screen.check(y, 'abc', 'F - test_undo_delete_text/baseline/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'def', 'F - test_undo_delete_text/baseline/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'xyz', 'F - test_undo_delete_text/baseline/screen:3')
-- undo
--? -- after undo, the backspaced key is selected
edit.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.top
App.screen.check(y, 'abc', 'F - test_undo_delete_text/screen:1')
y = y + Editor_state.line_height
App.screen.check(y, 'defg', 'F - test_undo_delete_text/screen:2')
y = y + Editor_state.line_height
App.screen.check(y, 'xyz', 'F - test_undo_delete_text/screen:3')
end
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 string
edit.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 cursor
Editor_state.cursor1 = {line=1, pos=1}
Editor_state.screen_top1 = {line=1, pos=1}
-- search for second occurrence
edit.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')
end
function 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 string
edit.run_after_keychord(Editor_state, 'C-f')
edit.run_after_textinput(Editor_state, 'a')
-- search for previous occurrence
edit.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')
end
function 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 string
edit.run_after_keychord(Editor_state, 'C-f')
edit.run_after_textinput(Editor_state, 'a')
edit.run_after_keychord(Editor_state, 'return')
-- cursor wraps
check_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')
end
function 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 string
edit.run_after_keychord(Editor_state, 'C-f')
edit.run_after_textinput(Editor_state, 'a')
edit.run_after_keychord(Editor_state, 'up')
-- cursor wraps
check_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 end
if line.mode ~= 'text' then return end
if State.lines[State.cursor1.line-1].mode == 'drawing' then
table.remove(State.lines, State.cursor1.line-1)
table.remove(State.line_cache, State.cursor1.line-1)
else
-- join lines
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1
State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data
table.remove(State.lines, State.cursor1.line)
table.remove(State.line_cache, State.cursor1.line)
end
if State.lines[State.cursor1.line+1].mode == 'text' then
-- join lines
State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data
-- delete side B on first line
State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB
end
table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB})
if State.lines[State.screen_top1.line].mode == 'text' then
y = y - State.line_height
elseif State.lines[State.screen_top1.line].mode == 'drawing' then
y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width)
end
State.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.line
while new_cursor_line > 1 do
new_cursor_line = new_cursor_line-1
if 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) - 1
break
end
local new_cursor_line = State.cursor1.line
while new_cursor_line > 1 do
new_cursor_line = new_cursor_line-1
if State.lines[new_cursor_line].mode == 'text' then
State.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) - 1
break
end
assert(State.lines[State.cursor1.line].mode == 'text')
local new_cursor_line = State.cursor1.line
while new_cursor_line < #State.lines do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.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)
break
end
State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB} -- copy
else
local new_cursor_line = State.cursor1.line
while new_cursor_line > 1 do
new_cursor_line = new_cursor_line-1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {
line = new_cursor_line,
pos = utf8.len(State.lines[new_cursor_line].data) + 1,
}
break
end
end
assert(State.lines[State.cursor1.line].mode == 'text')
else
local new_cursor_line = State.cursor1.line
while new_cursor_line <= #State.lines-1 do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {line=new_cursor_line, pos=1}
break
end
end
else
local new_cursor_line = State.cursor1.line
while new_cursor_line <= #State.lines-1 do
new_cursor_line = new_cursor_line+1
if State.lines[new_cursor_line].mode == 'text' then
State.cursor1 = {line=new_cursor_line, pos=1}
break
end
end
local y = State.top
while State.cursor1.line <= #State.lines do
if State.lines[State.cursor1.line].mode == 'text' then
break
end
--? 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 + 1
end
-- hack: insert a text line at bottom of file if necessary
if State.cursor1.line > #State.lines then
assert(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 then
if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then
local h = State.line_height
if y - h < State.top then
break
end
y = y - h
else
assert(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 then
break
end
--? print('skipping drawing of height', h)
y = y - h
if State.lines[loc1.line].mode == 'drawing' then
return {line=loc1.line, screen_line=1, screen_pos=1}
end
-- text editor, particularly text drawing, horizontal wrap, vertical scrolling
Text = {}
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 drawn
function 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 = y
line_cache.startpos = startpos
line_cache.startposB = startposB
-- draw A side
local overflows_screen, x, pos, screen_line_starting_pos
if startpos then
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_line(State, line_index, State.left, y, startpos)
if overflows_screen then
return y, screen_line_starting_pos
end
if Focus == 'edit' and State.cursor1.pos then
if State.search_term == nil then
if line_index == State.cursor1.line and State.cursor1.pos == pos then
Text.draw_cursor(State, x, y)
end
end
end
else
x = State.left
end
-- check for B side
--? if line_index == 8 then print('checking for B side') end
if line.dataB == nil then
assert(y)
assert(screen_line_starting_pos)
--? if line_index == 8 then print('return 1') end
return y, screen_line_starting_pos
end
if not State.expanded and not line.expanded then
assert(y)
assert(screen_line_starting_pos)
--? if line_index == 8 then print('return 2') end
button(State, 'expand', {x=x+AB_padding, y=y+2, w=App.width(State.em), h=State.line_height-4, color={1,1,1},
icon = function(button_params)
App.color(Fold_background_color)
love.graphics.rectangle('fill', button_params.x, button_params.y, App.width(State.em), State.line_height-4, 2,2)
end,
onpress1 = function()
line.expanded = true
end,
})
return y, screen_line_starting_pos
end
-- draw B side
--? if line_index == 8 then print('drawing B side') end
App.color(Fold_color)
if startposB then
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB)
else
overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x+AB_padding,y, 1)
end
if overflows_screen then
return y, nil, screen_line_starting_pos
end
--? if line_index == 8 then print('a') end
if Focus == 'edit' and State.cursor1.posB then
--? if line_index == 8 then print('b') end
if State.search_term == nil then
--? if line_index == 8 then print('c', State.cursor1.line, State.cursor1.posB, line_index, pos) end
if line_index == State.cursor1.line and State.cursor1.posB == pos then
Text.draw_cursor(State, x, y)
end
end
end
return y, nil, screen_line_starting_pos
end
-- Given an array of fragments, draw the subset starting from pos to screen
-- starting from (x,y).
-- Return:
-- - whether we got to bottom of screen before end of line
-- - the final (x,y)
-- - the final pos
-- - starting pos of the final screen line drawn
function Text.draw_wrapping_line(State, line_index, x,y, startpos)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
--? print('== line', line_index, '^'..line.data..'$')
local screen_line_starting_pos = startpos
Text.compute_fragments(State, line_index)
local pos = 1
initialize_color()
for _, f in ipairs(line_cache.fragments) do
App.color(Text_color)
local frag, frag_text = f.data, f.text
select_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 fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
assert(x > State.left) -- no overfull lines
y = y + State.line_height
if y + State.line_height > App.screen.height then
return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
end
screen_line_starting_pos = pos
x = State.left
end
App.screen.draw(frag_text, x,y)
-- render cursor if necessary
if State.cursor1.pos and line_index == State.cursor1.line then
if pos <= State.cursor1.pos and pos + frag_len > State.cursor1.pos then
if State.search_term then
if State.lines[State.cursor1.line].data:sub(State.cursor1.pos, State.cursor1.pos+utf8.len(State.search_term)-1) == State.search_term then
local 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)
end
elseif Focus == 'edit' then
Text.draw_cursor(State, x+Text.x(frag, State.cursor1.pos-pos+1), y)
App.color(Text_color)
end
end
end
x = x + frag_width
end
pos = pos + frag_len
end
return false, x,y, pos, screen_line_starting_pos
end
function Text.draw_wrapping_lineB(State, line_index, x,y, startpos)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
local screen_line_starting_pos = startpos
Text.compute_fragmentsB(State, line_index, x)
local pos = 1
for _, f in ipairs(line_cache.fragmentsB) do
local frag, frag_text = f.data, f.text
local frag_len = utf8.len(frag)
--? print('text.draw:', frag, 'at', line_index,pos, 'after', x,y)
if pos < startpos then
-- render nothing
--? print('skipping', frag)
else
-- render fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
assert(x > State.left) -- no overfull lines
y = y + State.line_height
if y + State.line_height > App.screen.height then
return --[[screen filled]] true, x,y, pos, screen_line_starting_pos
end
screen_line_starting_pos = pos
x = State.left
end
App.screen.draw(frag_text, x,y)
-- render cursor if necessary
if State.cursor1.posB and line_index == State.cursor1.line then
if pos <= State.cursor1.posB and pos + frag_len > State.cursor1.posB then
if State.search_term then
if State.lines[State.cursor1.line].dataB:sub(State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term)-1) == State.search_term then
local lo_px = Text.draw_highlight(State, line, x,y, pos, State.cursor1.posB, State.cursor1.posB+utf8.len(State.search_term))
App.color(Fold_color)
love.graphics.print(State.search_term, x+lo_px,y)
end
elseif Focus == 'edit' then
Text.draw_cursor(State, x+Text.x(frag, State.cursor1.posB-pos+1), y)
App.color(Fold_color)
end
end
end
x = x + frag_width
end
pos = pos + frag_len
end
return false, x,y, pos, screen_line_starting_pos
end
function Text.draw_cursor(State, x, y)
-- blink every 0.5s
if math.floor(Cursor_time*2)%2 == 0 then
App.color(Cursor_color)
love.graphics.rectangle('fill', x,y, 3,State.line_height)
end
State.cursor_x = x
State.cursor_y = y+State.line_height
end
function 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 then
return
end
-- duplicate some logic from Text.draw
Text.compute_fragments(State, line_index)
line_cache.screen_line_starting_pos = {1}
local x = State.left
local pos = 1
for _, f in ipairs(line_cache.fragments) do
local frag, frag_text = f.data, f.text
-- render fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
x = State.left
table.insert(line_cache.screen_line_starting_pos, pos)
end
x = x + frag_width
local frag_len = utf8.len(frag)
pos = pos + frag_len
end
end
function 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 then
return
end
line_cache.fragments = {}
local x = State.left
-- try to wrap at word boundaries
for frag in line.data:gmatch('%S*%s*') do
local frag_text = App.newText(love.graphics.getFont(), frag)
local frag_width = App.width(frag_text)
--? print('x: '..tostring(x)..'; 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 narrow
local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos
--? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
local frag1 = string.sub(frag, 1, boffset-1)
local frag1_text = App.newText(love.graphics.getFont(), frag1)
local frag1_width = App.width(frag1_text)
--? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
assert(x + frag1_width <= State.right)
table.insert(line_cache.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)
end
x = State.left -- new line
end
if #frag > 0 then
--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
table.insert(line_cache.fragments, {data=frag, text=frag_text})
end
x = x + frag_width
end
end
function Text.populate_screen_line_starting_posB(State, line_index, x)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
if line_cache.screen_line_starting_posB then
return
end
-- duplicate some logic from Text.draw
Text.compute_fragmentsB(State, line_index, x)
line_cache.screen_line_starting_posB = {1}
local pos = 1
for _, f in ipairs(line_cache.fragmentsB) do
local frag, frag_text = f.data, f.text
-- render fragment
local frag_width = App.width(frag_text)
if x + frag_width > State.right then
x = State.left
table.insert(line_cache.screen_line_starting_posB, pos)
end
x = x + frag_width
local frag_len = utf8.len(frag)
pos = pos + frag_len
end
end
function Text.compute_fragmentsB(State, line_index, x)
--? print('compute_fragmentsB', line_index, 'between', x, State.right)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
if line_cache.fragmentsB then
return
end
line_cache.fragmentsB = {}
-- try to wrap at word boundaries
for frag in line.dataB:gmatch('%S*%s*') do
local frag_text = App.newText(love.graphics.getFont(), frag)
local frag_width = App.width(frag_text)
--? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go')
while x + frag_width > State.right do
--? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x))
if (x-State.left) < 0.8 * (State.right-State.left) then
--? print('splitting')
-- long word; chop it at some letter
-- We're not going to reimplement TeX here.
local bpos = Text.nearest_pos_less_than(frag, State.right - x)
--? print('bpos', bpos)
if bpos == 0 then break end -- avoid infinite loop when window is too narrow
local boffset = Text.offset(frag, bpos+1) -- byte _after_ bpos
--? print('space for '..tostring(bpos)..' graphemes, '..tostring(boffset-1)..' bytes')
local frag1 = string.sub(frag, 1, boffset-1)
local frag1_text = App.newText(love.graphics.getFont(), frag1)
local frag1_width = App.width(frag1_text)
--? print('extracting ^'..frag1..'$ of width '..tostring(frag1_width)..'px')
assert(x + frag1_width <= State.right)
table.insert(line_cache.fragmentsB, {data=frag1, text=frag1_text})
frag = string.sub(frag, boffset)
frag_text = App.newText(love.graphics.getFont(), frag)
frag_width = App.width(frag_text)
end
x = State.left -- new line
end
if #frag > 0 then
--? print('inserting ^'..frag..'$ of width '..tostring(frag_width)..'px')
table.insert(line_cache.fragmentsB, {data=frag, text=frag_text})
end
x = x + frag_width
end
end
function Text.textinput(State, t)
if App.mouse_down(1) then return end
if App.ctrl_down() or App.alt_down() or App.cmd_down() then return end
local 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 then
Text.populate_screen_line_starting_pos(State, State.cursor1.line)
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
end
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
end
function Text.insert_at_cursor(State, t)
if State.cursor1.pos then
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.pos = State.cursor1.pos+1
else
assert(State.cursor1.posB)
local byte_offset = Text.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].dataB, byte_offset)
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1.posB = State.cursor1.posB+1
end
end
-- Don't handle any keys here that would trigger love.textinput above.
function Text.keychord_pressed(State, chord)
--? print('chord', chord)
--== shortcuts that mutate text
if chord == 'return' then
local before_line = State.cursor1.line
local before = snapshot(State, before_line)
Text.insert_return(State)
if State.cursor_y > App.screen.height - State.line_height then
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
end
schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
elseif chord == 'tab' then
local 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 then
Text.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)
end
schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
elseif chord == 'backspace' then
local before
if State.cursor1.pos and State.cursor1.pos > 1 then
before = 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 then
if byte_end then
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)
else
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_start-1)
end
State.cursor1.pos = State.cursor1.pos-1
end
elseif State.cursor1.posB then
if State.cursor1.posB > 1 then
before = snapshot(State, State.cursor1.line)
local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1)
local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
if byte_start then
if byte_end then
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
else
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
end
State.cursor1.posB = State.cursor1.posB-1
end
else
-- refuse to delete past beginning of side B
end
elseif State.cursor1.line > 1 then
before = snapshot(State, State.cursor1.line-1, State.cursor1.line)
end
State.cursor1.line = State.cursor1.line-1
end
if State.screen_top1.line > #State.lines then
Text.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) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2, State.left, State.right)
State.screen_top1 = Text.to1(State, top2)
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
end
Text.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' then
local before
if State.cursor1.posB or State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
before = snapshot(State, State.cursor1.line)
else
before = snapshot(State, State.cursor1.line, State.cursor1.line+1)
end
if State.cursor1.pos and State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
local 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 then
if byte_end then
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)
else
State.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 then
if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then
local byte_start = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB)
local byte_end = utf8.offset(State.lines[State.cursor1.line].dataB, State.cursor1.posB+1)
if byte_start then
if byte_end then
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)..string.sub(State.lines[State.cursor1.line].dataB, byte_end)
else
State.lines[State.cursor1.line].dataB = string.sub(State.lines[State.cursor1.line].dataB, 1, byte_start-1)
end
-- no change to State.cursor1.pos
end
else
-- refuse to delete past end of side B
end
elseif State.cursor1.line < #State.lines then
table.remove(State.lines, State.cursor1.line+1)
table.remove(State.line_cache, State.cursor1.line+1)
end
Text.clear_screen_line_cache(State, State.cursor1.line)
schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
--== shortcuts that move the cursor
elseif chord == 'left' then
Text.left(State)
elseif chord == 'right' then
Text.right(State)
elseif chord == 'S-left' then
Text.left(State)
elseif chord == 'S-right' then
Text.right(State)
-- C- hotkeys reserved for drawings, so we'll use M-
elseif chord == 'M-left' then
Text.word_left(State)
elseif chord == 'M-right' then
Text.word_right(State)
elseif chord == 'M-S-left' then
Text.word_left(State)
elseif chord == 'M-S-right' then
Text.word_right(State)
elseif chord == 'home' then
Text.start_of_line(State)
elseif chord == 'end' then
Text.end_of_line(State)
elseif chord == 'S-home' then
Text.start_of_line(State)
elseif chord == 'S-end' then
Text.end_of_line(State)
elseif chord == 'up' then
Text.up(State)
elseif chord == 'down' then
Text.down(State)
elseif chord == 'S-up' then
Text.up(State)
elseif chord == 'S-down' then
Text.down(State)
elseif chord == 'pageup' then
Text.pageup(State)
elseif chord == 'pagedown' then
Text.pagedown(State)
elseif chord == 'S-pageup' then
Text.pageup(State)
elseif chord == 'S-pagedown' then
Text.pagedown(State)
end
end
function Text.insert_return(State)
if State.cursor1.pos then
-- when inserting a newline, move any B side to the new line
local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)
table.insert(State.line_cache, State.cursor1.line+1, {})
State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)
State.lines[State.cursor1.line].dataB = nil
Text.clear_screen_line_cache(State, State.cursor1.line)
State.cursor1 = {line=State.cursor1.line+1, pos=1}
else
-- disable enter when cursor is on the B side
end
end
function Text.pageup(State)
--? print('pageup')
-- duplicate some logic from love.draw
local top2 = Text.to2(State, State.screen_top1)
--? print(App.screen.height)
local y = App.screen.height - State.line_height
while y >= State.top do
--? print(y, top2.line, top2.screen_line, top2.screen_pos)
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)
end
State.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')
end
function 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) then
State.screen_top1 = new_top1
else
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')
end
function Text.up(State)
if State.cursor1.pos then
Text.upA(State)
else
Text.upB(State)
end
end
function 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
end
else
-- move up one screen line in current line
assert(screen_line_index > 1)
local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]
local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
--? print('cursor pos is now '..tostring(State.cursor1.pos))
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
end
function Text.upB(State)
local line_cache = State.line_cache[State.cursor1.line]
local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)
assert(screen_line_indexB >= 1)
if screen_line_indexB == 1 then
-- move to A side of previous line
end
elseif screen_line_indexB == 2 then
-- all-B screen-line to potentially A+B screen-line
local xA = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
if State.cursor_x < xA then
State.cursor1.posB = nil
Text.populate_screen_line_starting_pos(State, State.cursor1.line)
local new_screen_line_starting_pos = line_cache.screen_line_starting_pos[#line_cache.screen_line_starting_pos]
local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)
State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
else
Text.populate_screen_line_starting_posB(State, State.cursor1.line)
local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x-xA, State.left) - 1
end
else
assert(screen_line_indexB > 2)
-- all-B screen-line to all-B screen-line
Text.populate_screen_line_starting_posB(State, State.cursor1.line)
local new_screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB-1]
local new_screen_line_starting_byte_offsetB = Text.offset(State.lines[State.cursor1.line].dataB, new_screen_line_starting_posB)
local s = string.sub(State.lines[State.cursor1.line].dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
end
-- cursor on final screen line (A or B side) => goes to next screen line on A side
-- cursor on A side => move down one screen line (A side) in current line
-- cursor on B side => move down one screen line (B side) in current line
function 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')
end
if State.cursor1.line > State.screen_bottom1.line then
--? print('screen top before:', State.screen_top1.line, State.screen_top1.pos)
--? print('scroll up preserving cursor')
Text.snap_cursor_to_bottom_of_screen(State)
--? print('screen top after:', State.screen_top1.line, State.screen_top1.pos)
end
elseif State.cursor1.pos then
-- move down one screen line (A side) in current line
local scroll_down = Text.le1(State.screen_bottom1, State.cursor1)
--? print('cursor is NOT at final screen line of its line')
local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)
Text.populate_screen_line_starting_pos(State, State.cursor1.line)
local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index+1]
--? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos))
local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)
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)
end
else
-- move down one screen line (B side) in current line
local scroll_down = false
if Text.le1(State.screen_bottom1, State.cursor1) then
scroll_down = true
end
local cursor_line = State.lines[State.cursor1.line]
local cursor_line_cache = State.line_cache[State.cursor1.line]
local cursor2 = Text.to2(State, State.cursor1)
assert(cursor2.screen_lineB < #cursor_line_cache.screen_line_starting_posB)
local screen_line_starting_posB, screen_line_indexB = Text.pos_at_start_of_screen_lineB(State, State.cursor1)
Text.populate_screen_line_starting_posB(State, State.cursor1.line)
local new_screen_line_starting_posB = cursor_line_cache.screen_line_starting_posB[screen_line_indexB+1]
local new_screen_line_starting_byte_offsetB = Text.offset(cursor_line.dataB, new_screen_line_starting_posB)
local s = string.sub(cursor_line.dataB, new_screen_line_starting_byte_offsetB)
State.cursor1.posB = new_screen_line_starting_posB + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1
if scroll_down then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
--? print('=>', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)
end
function Text.start_of_line(State)
if State.cursor1.pos then
State.cursor1.pos = 1
else
State.cursor1.posB = 1
end
if Text.lt1(State.cursor1, State.screen_top1) then
end
end
function Text.end_of_line(State)
if State.cursor1.pos then
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
else
State.cursor1.posB = utf8.len(State.lines[State.cursor1.line].dataB) + 1
end
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.word_left(State)
-- we can cross the fold, so check side A/B one level down
Text.skip_whitespace_left(State)
Text.left(State)
Text.skip_non_whitespace_left(State)
end
function Text.word_right(State)
-- we can cross the fold, so check side A/B one level down
Text.skip_whitespace_right(State)
Text.right(State)
Text.skip_non_whitespace_right(State)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.skip_whitespace_left(State)
if State.cursor1.pos then
Text.skip_whitespace_leftA(State)
else
Text.skip_whitespace_leftB(State)
end
end
function Text.skip_non_whitespace_left(State)
if State.cursor1.pos then
Text.skip_non_whitespace_leftA(State)
Text.skip_non_whitespace_leftB(State)
end
end
function Text.skip_whitespace_leftA(State)
while true do
if State.cursor1.pos == 1 then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%S') then
break
end
Text.left(State)
end
end
function Text.skip_whitespace_leftB(State)
while true do
if State.cursor1.posB == 1 then
break
end
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%S') then
break
end
Text.left(State)
end
end
function Text.skip_non_whitespace_leftA(State)
while true do
if State.cursor1.pos == 1 then
break
end
assert(State.cursor1.pos > 1)
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
break
end
Text.left(State)
end
end
function Text.skip_non_whitespace_leftB(State)
while true do
if State.cursor1.posB == 1 then
break
end
assert(State.cursor1.posB > 1)
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB-1, '%s') then
break
end
Text.left(State)
end
end
function Text.skip_whitespace_right(State)
if State.cursor1.pos then
Text.skip_whitespace_rightA(State)
else
Text.skip_whitespace_rightB(State)
end
end
function Text.skip_non_whitespace_right(State)
if State.cursor1.pos then
Text.skip_non_whitespace_rightA(State)
else
Text.skip_non_whitespace_rightB(State)
end
end
function Text.skip_whitespace_rightA(State)
while true do
if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%S') then
break
end
Text.right_without_scroll(State)
end
end
function Text.skip_whitespace_rightB(State)
while true do
if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
break
end
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%S') then
break
end
Text.right_without_scroll(State)
end
end
function Text.skip_non_whitespace_rightA(State)
while true do
if State.cursor1.pos > utf8.len(State.lines[State.cursor1.line].data) then
break
end
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos, '%s') then
break
end
Text.right_without_scroll(State)
end
end
function Text.skip_non_whitespace_rightB(State)
while true do
if State.cursor1.posB > utf8.len(State.lines[State.cursor1.line].dataB) then
break
end
if Text.match(State.lines[State.cursor1.line].dataB, State.cursor1.posB, '%s') then
break
end
Text.right_without_scroll(State)
end
end
function 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)
end
function Text.left(State)
if State.cursor1.pos then
Text.leftA(State)
else
Text.leftB(State)
end
end
function Text.leftA(State)
if State.cursor1.pos > 1 then
State.cursor1.pos = State.cursor1.pos-1
else
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
end
function Text.leftB(State)
if State.cursor1.posB > 1 then
State.cursor1.posB = State.cursor1.posB-1
else
-- overflow back into A side
State.cursor1.posB = nil
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1
end
if Text.lt1(State.cursor1, State.screen_top1) then
local top2 = Text.to2(State, State.screen_top1)
top2 = Text.previous_screen_line(State, top2)
State.screen_top1 = Text.to1(State, top2)
end
end
function Text.right(State)
Text.right_without_scroll(State)
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State)
end
end
function Text.right_without_scroll(State)
end
end
end
function Text.pos_at_start_of_screen_line(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line]
for i=#line_cache.screen_line_starting_pos,1,-1 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
return spos,i
end
end
assert(false)
end
function Text.pos_at_start_of_screen_lineB(State, loc1)
Text.populate_screen_line_starting_pos(State, loc1.line)
local line_cache = State.line_cache[loc1.line]
local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, loc1.line, x)
for i=#line_cache.screen_line_starting_posB,1,-1 do
local sposB = line_cache.screen_line_starting_posB[i]
if sposB <= loc1.posB then
return sposB,i
end
assert(false)
end
function 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) or
line.dataB == nil then
return screen_lines[#screen_lines] <= State.cursor1.pos
end
if State.cursor1.pos then
-- ignore B side
return screen_lines[#screen_lines] <= State.cursor1.pos
end
assert(State.cursor1.posB)
local line_cache = State.line_cache[State.cursor1.line]
local x = Margin_left + Text.screen_line_width(State, State.cursor1.line, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, State.cursor1.line, x)
local screen_lines = State.line_cache[State.cursor1.line].screen_line_starting_posB
return screen_lines[#screen_lines] <= State.cursor1.posB
end
function 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)
end
end
-- should never modify State.cursor1
function 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 line
if top2.screen_pos then
top2.screen_pos = 1
else
assert(top2.screen_posB)
top2.screen_posB = 1
end
--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB)
--? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down')
local y = App.screen.height - State.line_height
-- duplicate some logic from love.draw
while 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
end
top2 = 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 leaks
end
function 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 page
if y < line_cache.starty then return false end
local num_screen_lines = 0
if line_cache.startpos then
Text.populate_screen_line_starting_pos(State, line_index)
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_pos - Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos) + 1
end
--? print('#screenlines after A', num_screen_lines)
if line.dataB and (State.expanded or line.expanded) then
local x = Margin_left + Text.screen_line_width(State, line_index, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, line_index, x)
--? print('B:', x, #line_cache.screen_line_starting_posB)
if line_cache.startposB then
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB) -- no +1; first screen line of B side overlaps with A side
else
num_screen_lines = num_screen_lines + #line_cache.screen_line_starting_posB - Text.screen_line_indexB(line_cache.screen_line_starting_posB, 1) -- no +1; first screen line of B side overlaps with A side
end
end
--? print('#screenlines after B', num_screen_lines)
return y < line_cache.starty + State.line_height*num_screen_lines
end
-- 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 -> dataB
function 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.draw
local 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 then
local start_screen_line_index = Text.screen_line_index(line_cache.screen_line_starting_pos, line_cache.startpos)
for screen_line_index = start_screen_line_index,#line_cache.screen_line_starting_pos do
local screen_line_starting_pos = line_cache.screen_line_starting_pos[screen_line_index]
local screen_line_starting_byte_offset = Text.offset(line.data, screen_line_starting_pos)
--? print('iter', y, screen_line_index, screen_line_starting_pos, string.sub(line.data, screen_line_starting_byte_offset))
local nexty = y + State.line_height
if my < nexty then
-- On all wrapped screen lines but the final one, clicks past end of
-- line position cursor on final character of screen line.
-- (The final screen line positions past end of screen line as always.)
if screen_line_index < #line_cache.screen_line_starting_pos and mx > State.left + Text.screen_line_width(State, line_index, screen_line_index) then
--? print('past end of non-final line; return')
return line_cache.screen_line_starting_pos[screen_line_index+1]-1
end
local s = string.sub(line.data, screen_line_starting_byte_offset)
--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_pos + Text.nearest_cursor_pos(s, mx, State.left) - 1)
local screen_line_posA = Text.nearest_cursor_pos(s, mx, State.left)
if line.dataB == nil then
-- no B side
return screen_line_starting_pos + screen_line_posA - 1
end
if not State.expanded and not line.expanded then
-- B side is not expanded
return screen_line_starting_pos + screen_line_posA - 1
end
local lenA = utf8.len(s)
if screen_line_posA < lenA then
-- mx is within A side
return screen_line_starting_pos + screen_line_posA - 1
end
local max_xA = State.left+Text.x(s, lenA+1)
if mx < max_xA + AB_padding then
-- mx is in the space between A and B side
return screen_line_starting_pos + screen_line_posA - 1
end
mx = mx - max_xA - AB_padding
local screen_line_posB = Text.nearest_cursor_pos(line.dataB, mx, --[[no left margin]] 0)
return nil, screen_line_posB
end
y = nexty
end
end
-- look in screen lines composed entirely of the B side
assert(State.expanded or line.expanded)
local start_screen_line_indexB
if line_cache.startposB then
start_screen_line_indexB = Text.screen_line_indexB(line_cache.screen_line_starting_posB, line_cache.startposB)
else
start_screen_line_indexB = 2 -- skip the first line of side B, which we checked above
end
for screen_line_indexB = start_screen_line_indexB,#line_cache.screen_line_starting_posB do
local screen_line_starting_posB = line_cache.screen_line_starting_posB[screen_line_indexB]
local screen_line_starting_byte_offsetB = Text.offset(line.dataB, screen_line_starting_posB)
--? print('iter2', y, screen_line_indexB, screen_line_starting_posB, string.sub(line.dataB, screen_line_starting_byte_offsetB))
local nexty = y + State.line_height
if my < nexty then
-- On all wrapped screen lines but the final one, clicks past end of
-- line position cursor on final character of screen line.
-- (The final screen line positions past end of screen line as always.)
--? 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]-1
end
local s = string.sub(line.dataB, screen_line_starting_byte_offsetB)
--? print('return', mx, Text.nearest_cursor_pos(s, mx, State.left), '=>', screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1)
return nil, screen_line_starting_posB + Text.nearest_cursor_pos(s, mx, State.left) - 1
end
y = nexty
end
assert(false)
end
function 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_line
if i < #line_cache.screen_line_starting_pos then
local 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)
else
screen_line = string.sub(line.data, start_pos)
end
local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
return App.width(screen_line_text)
end
function Text.screen_line_widthB(State, line_index, i)
local line = State.lines[line_index]
local line_cache = State.line_cache[line_index]
local start_posB = line_cache.screen_line_starting_posB[i]
local start_offsetB = Text.offset(line.dataB, start_posB)
local screen_line
if i < #line_cache.screen_line_starting_posB then
--? print('non-final', i)
local past_end_posB = line_cache.screen_line_starting_posB[i+1]
local past_end_offsetB = Text.offset(line.dataB, past_end_posB)
--? print('between', start_offsetB, past_end_offsetB)
screen_line = string.sub(line.dataB, start_offsetB, past_end_offsetB-1)
else
--? print('final', i)
--? print('after', start_offsetB)
screen_line = string.sub(line.dataB, start_offsetB)
end
local screen_line_text = App.newText(love.graphics.getFont(), screen_line)
--? local result = App.width(screen_line_text)
--? print('=>', result)
--? return result
return App.width(screen_line_text)
end
function Text.screen_line_index(screen_line_starting_pos, pos)
for i = #screen_line_starting_pos,1,-1 do
if screen_line_starting_pos[i] <= pos then
return i
end
end
end
function Text.screen_line_indexB(screen_line_starting_posB, posB)
if posB == nil then
return 0
end
assert(screen_line_starting_posB)
for i = #screen_line_starting_posB,1,-1 do
if screen_line_starting_posB[i] <= posB then
return i
end
end
end
-- convert x pixel coordinate to pos
-- oblivious to wrapping
-- result: 1 to len+1
function Text.nearest_cursor_pos(line, x, left)
if x < left then
return 1
end
local len = utf8.len(line)
local max_x = left+Text.x(line, len+1)
if x > max_x then
return len+1
end
local leftpos, rightpos = 1, len+1
--? print('-- nearest', x)
while true do
--? print('nearest', x, '^'..line..'$', leftpos, rightpos)
if leftpos == rightpos then
return leftpos
end
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, leftpos, rightpos, curr, currxmin, currxmax)
if currxmin <= x and x < currxmax then
if x-currxmin < currxmax-x then
return curr
else
return curr+1
end
end
if leftpos >= rightpos-1 then
return rightpos
end
if currxmin > x then
rightpos = curr
else
leftpos = curr
end
end
assert(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+1
function 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 then
return len+1
end
local left, right = 0, len+1
while true do
local 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 then
return curr
end
if left >= right-1 then
return left
end
if currxmin > x then
right = curr
else
left = curr
end
end
assert(false)
end
function 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)
end
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)
end
function Text.to2(State, loc1)
end
function Text.to2A(State, loc1)
local result = {line=loc1.line}
local line_cache = State.line_cache[loc1.line]
Text.populate_screen_line_starting_pos(State, loc1.line)
for i=#line_cache.screen_line_starting_pos,1,-1 do
local spos = line_cache.screen_line_starting_pos[i]
if spos <= loc1.pos then
result.screen_line = i
result.screen_pos = loc1.pos - spos + 1
break
end
end
assert(result.screen_pos)
return result
end
function Text.to2B(State, loc1)
local result = {line=loc1.line}
local line_cache = State.line_cache[loc1.line]
Text.populate_screen_line_starting_pos(State, loc1.line)
local x = Margin_left + Text.screen_line_width(State, loc1.line, #line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, loc1.line, x)
for i=#line_cache.screen_line_starting_posB,1,-1 do
local sposB = line_cache.screen_line_starting_posB[i]
if sposB <= loc1.posB then
result.screen_lineB = i
result.screen_posB = loc1.posB - sposB + 1
break
end
end
assert(result.screen_posB)
return result
end
function Text.to1(State, loc2)
if loc2.screen_pos then
return Text.to1A(State, loc2)
else
return Text.to1B(State, loc2)
end
end
function Text.to1A(State, loc2)
local result = {line=loc2.line, pos=loc2.screen_pos}
if loc2.screen_line > 1 then
result.pos = State.line_cache[loc2.line].screen_line_starting_pos[loc2.screen_line] + loc2.screen_pos - 1
end
return result
end
function Text.to1B(State, loc2)
local result = {line=loc2.line, posB=loc2.screen_posB}
if loc2.screen_lineB > 1 then
result.posB = State.line_cache[loc2.line].screen_line_starting_posB[loc2.screen_lineB] + loc2.screen_posB - 1
end
return result
end
function Text.lt1(a, b)
if a.line < b.line then
return true
end
if a.line > b.line then
return false
end
-- A side < B side
if a.pos and not b.pos then
return true
end
if not a.pos and b.pos then
return false
end
if a.pos then
return a.pos < b.pos
else
return a.posB < b.posB
end
end
function Text.le1(a, b)
return eq(a, b) or Text.lt1(a, b)
end
function Text.offset(s, pos1)
if pos1 == 1 then return 1 end
local result = utf8.offset(s, pos1)
if result == nil then
print(pos1, #s, s)
end
assert(result)
return result
end
function Text.previous_screen_line(State, loc2)
if loc2.screen_pos then
return Text.previous_screen_lineA(State, loc2)
else
return Text.previous_screen_lineB(State, loc2)
end
end
function Text.previous_screen_lineA(State, loc2)
if loc2.screen_line > 1 then
return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1}
elseif loc2.line == 1 then
return loc2
else
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) then
--? print('c1', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, State.line_cache[loc2.line-1].fragmentsB)
return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
end
-- try to switch to B
local prev_line_cache = State.line_cache[loc2.line-1]
local x = Margin_left + Text.screen_line_width(State, loc2.line-1, #prev_line_cache.screen_line_starting_pos) + AB_padding
Text.populate_screen_line_starting_posB(State, loc2.line-1, x)
local screen_line_starting_posB = State.line_cache[loc2.line-1].screen_line_starting_posB
--? print('c', loc2.line-1, State.lines[loc2.line-1].data, '==', State.lines[loc2.line-1].dataB, '==', #screen_line_starting_posB, 'starting from x', x)
if #screen_line_starting_posB > 1 then
--? print('c2')
return {line=loc2.line-1, screen_lineB=#State.line_cache[loc2.line-1].screen_line_starting_posB, screen_posB=1}
else
--? print('c3')
-- if there's only one screen line, assume it overlaps with A, so remain in A
return {line=loc2.line-1, screen_line=#State.line_cache[loc2.line-1].screen_line_starting_pos, screen_pos=1}
end
end
end
function Text.previous_screen_lineB(State, loc2)
if loc2.screen_lineB > 2 then -- first screen line of B side overlaps with A side
return {line=loc2.line, screen_lineB=loc2.screen_lineB-1, screen_posB=1}
else
-- switch to A side
-- TODO: handle case where fold lands precisely at end of a new screen-line
return {line=loc2.line, screen_line=#State.line_cache[loc2.line].screen_line_starting_pos, screen_pos=1}
end
end
-- resize helper
function Text.tweak_screen_top_and_cursor(State)
if State.screen_top1.pos == 1 then return end
Text.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 do
local pos = line_cache.screen_line_starting_pos[i]
if pos == State.screen_top1.pos then
break
end
if pos > State.screen_top1.pos then
-- make sure screen top is at start of a screen line
local prev = line_cache.screen_line_starting_pos[i-1]
if State.screen_top1.pos - prev < pos - State.screen_top1.pos then
State.screen_top1.pos = prev
else
State.screen_top1.pos = pos
end
break
end
end
-- make sure cursor is on screen
if Text.lt1(State.cursor1, State.screen_top1) then
State.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}
end
end
end
-- slightly expensive since it redraws the screen
function 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)
end
function Text.redraw_all(State)
--? print('clearing fragments')
State.line_cache = {}
for i=1,#State.lines do
State.line_cache[i] = {}
end
end
function Text.clear_screen_line_cache(State, line_index)
State.line_cache[line_index].fragments = nil
State.line_cache[line_index].fragmentsB = nil
State.line_cache[line_index].screen_line_starting_pos = nil
State.line_cache[line_index].screen_line_starting_posB = nil
end
function trim(s)
return s:gsub('^%s+', ''):gsub('%s+$', '')
end
function ltrim(s)
return s:gsub('^%s+', '')
end
function rtrim(s)
return s:gsub('%s+$', '')
end
if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated
table.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..']*)')
else
line_info.data = line
end
table.insert(result, line_info)
table.insert(result, {mode='text', data=''})
if line.mode == 'drawing' then
store_drawing(outfile, line)
else
outfile:write(line.data)
if line.dataB and #line.dataB > 0 then
outfile:write(Fold)
outfile:write(line.dataB)
end
outfile:write('\n')
end
function load_drawing(infile_next_line)
local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}}
while true do
local line = infile_next_line()
assert(line)
if line == '```' then break end
local shape = json.decode(line)
if shape.mode == 'freehand' then
-- no changes needed
elseif shape.mode == 'line' or shape.mode == 'manhattan' then
local name = shape.p1.name
shape.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 = name
name = shape.p2.name
shape.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 = name
elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
for i,p in ipairs(shape.vertices) do
local name = p.name
shape.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 = name
end
elseif shape.mode == 'circle' or shape.mode == 'arc' then
local name = shape.center.name
shape.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 = name
elseif shape.mode == 'deleted' then
-- ignore
else
print(shape.mode)
assert(false)
end
table.insert(drawing.shapes, shape)
end
return drawing
end
function store_drawing(outfile, drawing)
outfile:write('```lines\n')
for _,shape in ipairs(drawing.shapes) do
if shape.mode == 'freehand' then
outfile:write(json.encode(shape), '\n')
elseif shape.mode == 'line' or shape.mode == 'manhattan' then
local 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' then
local obj = {mode=shape.mode, vertices={}}
for _,p in ipairs(shape.vertices) do
table.insert(obj.vertices, drawing.points[p])
end
local line = json.encode(obj)
outfile:write(line, '\n')
elseif shape.mode == 'circle' then
outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius}), '\n')
elseif shape.mode == 'arc' then
outfile: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
-- ignore
else
print(shape.mode)
assert(false)
end
end
outfile: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..']*)')
else
line_info.data = line
end
table.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 line
while true do
i, line = iter(a, i)
assert(i)
--? print(i)
if line == '```' then break end
local shape = json.decode(line)
if shape.mode == 'freehand' then
-- no changes needed
elseif shape.mode == 'line' or shape.mode == 'manhattan' then
local name = shape.p1.name
shape.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 = name
name = shape.p2.name
shape.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 = name
elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
for i,p in ipairs(shape.vertices) do
local name = p.name
shape.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 = name
end
elseif shape.mode == 'circle' or shape.mode == 'arc' then
local name = shape.center.name
shape.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 = name
elseif shape.mode == 'deleted' then
-- ignore
else
print(shape.mode)
assert(false)
end
table.insert(drawing.shapes, shape)
end
return i, drawing
end
-- primitives for saving to file and loading from file
Fold = '\x1e' -- ASCII RS (record separator)
function file_exists(filename)
local infile = App.open_for_reading(filename)
if infile then
infile:close()
return true
else
return false
end
end
function load_from_disk(State)
local infile = App.open_for_reading(State.filename)
State.lines = load_from_file(infile)
if infile then infile:close() end
end
function load_from_file(infile)
local result = {}
if infile then
local infile_next_line = infile:lines() -- works with both Lua files and LÖVE Files (https://www.love2d.org/wiki/File)
while true do
local 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 drawn
Current_name_background_color = {r=1, g=0, b=0, a=0.1} -- name currently being edited
Icon_color = {r=0.7, g=0.7, b=0.7} -- color of current mode icon in drawings
Help_color = {r=0, g=0.5, b=0}
Help_background_color = {r=0, g=0.5, b=0, a=0.1}
Drawing_padding_top = 10
Drawing_padding_bottom = 10
Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom
Same_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 lines
current_drawing_mode = 'line',
previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points
function edit.fixup_cursor(State)
for i,line in ipairs(State.lines) do
if line.mode == 'text' then
State.cursor1.line = i
break
end
end
end
--? print('draw:', y, line_index, line, line.mode)
if line.mode == 'text' then
local startpos, startposB = 1, nil
if line_index == State.screen_top1.line then
if State.screen_top1.pos then
startpos = State.screen_top1.pos
else
startpos, startposB = nil, State.screen_top1.posB
end
end
if line.data == '' then
-- button to insert new drawing
button(State, 'draw', {x=4,y=y+4, w=12,h=12, color={1,1,0},
icon = icon.insert_drawing,
onpress1 = function()
Drawing.before = snapshot(State, line_index-1, line_index)
table.insert(State.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})
table.insert(State.line_cache, line_index, {})
if State.cursor1.line >= line_index then
State.cursor1.line = State.cursor1.line+1
end
schedule_save(State)
record_undo_event(State, {before=Drawing.before, after=snapshot(State, line_index-1, line_index+1)})
end,
})
y, 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' then
y = y+Drawing_padding_top
Drawing.draw(State, line_index, y)
y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom
else
print(line.mode)
assert(false)
Drawing.update(State, dt)
--? print('press')
if line.mode == 'text' then
if Text.in_line(State, line_index, x,y) then
local pos,posB = Text.to_pos_on_line(State, line_index, x, y)
--? print(x,y, 'setting cursor:', line_index, pos, posB)
State.cursor1 = {line=line_index, pos=pos, posB=posB}
break
end
elseif line.mode == 'drawing' then
local line_cache = State.line_cache[line_index]
if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) then
State.lines.current_drawing_index = line_index
State.lines.current_drawing = line
Drawing.before = snapshot(State, line_index)
Drawing.mouse_pressed(State, line_index, x,y, mouse_button)
break
end
if State.search_term then return end
--? print('release')
if State.lines.current_drawing then
Drawing.mouse_released(State, x,y, mouse_button)
schedule_save(State)
if Drawing.before then
record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)})
Drawing.before = nil
end
end
elseif State.current_drawing_mode == 'name' then
local before = snapshot(State, State.lines.current_drawing_index)
local drawing = State.lines.current_drawing
local p = drawing.points[drawing.pending.target_point]
p.name = p.name..t
record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
elseif chord == 'C-i' then
-- invalidate various cached bits of lines
State.lines.current_drawing = nil
-- invalidate various cached bits of lines
State.lines.current_drawing = nil
-- dispatch to drawing or text
elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then
-- DON'T reset line_cache.starty here
local drawing_index, drawing = Drawing.current_drawing(State)
if drawing_index then
local before = snapshot(State, drawing_index)
Drawing.keychord_pressed(State, chord)
record_undo_event(State, {before=before, after=snapshot(State, drawing_index)})
schedule_save(State)
end
elseif chord == 'escape' and not App.mouse_down(1) then
for _,line in ipairs(State.lines) do
if line.mode == 'drawing' then
line.show_help = false
end
end
elseif State.current_drawing_mode == 'name' then
if chord == 'return' then
State.current_drawing_mode = State.previous_drawing_mode
State.previous_drawing_mode = nil
else
local before = snapshot(State, State.lines.current_drawing_index)
local drawing = State.lines.current_drawing
local p = drawing.points[drawing.pending.target_point]
if chord == 'escape' then
p.name = nil
record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
elseif chord == 'backspace' then
local len = utf8.len(p.name)
local byte_offset = Text.offset(p.name, len-1)
if len == 1 then byte_offset = 0 end
p.name = string.sub(p.name, 1, byte_offset)
record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)})
end
end
schedule_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 line
line_cache = {},
-- Given wrapping, any potential location for the text cursor can be described in two ways:
-- * schema 1: As a combination of line index and position within a line (in utf8 codepoint units)
-- * schema 2: As a combination of line index, screen line index within the line, and a position within the screen line.
-- 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 screen
cursor1 = {line=1, pos=1, posB=nil}, -- position of cursor
screen_bottom1 = {line=1, pos=1, posB=nil}, -- position of start of screen line at bottom of screen
-- cursor coordinates in pixels
cursor_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) then
print(State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB)
assert(false)
end
State.cursor_x = nil
State.cursor_y = nil
local y = State.top
--? print('== draw')
for line_index = State.screen_top1.line,#State.lines do
local line = State.lines[line_index]
if State.next_save and State.next_save < App.getTime() then
save_to_disk(State)
State.next_save = nil
end
end
function schedule_save(State)
if State.next_save == nil then
State.next_save = App.getTime() + 3 -- short enough that you're likely to still remember what you did
end
end
function edit.quit(State)
-- make sure to save before quitting
if State.next_save then
save_to_disk(State)
end
end
function edit.mouse_pressed(State, x,y, mouse_button)
if State.search_term then return end
else
Text.textinput(State, t)
end
schedule_save(State)
end
function edit.keychord_pressed(State, chord, key)
if State.search_term then
if chord == 'escape' then
State.search_term = nil
State.search_text = nil
State.cursor1 = State.search_backup.cursor
State.screen_top1 = State.search_backup.screen_top
State.search_backup = nil
Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks
elseif chord == 'return' then
State.search_term = nil
State.search_text = nil
State.search_backup = nil
elseif chord == 'backspace' then
local len = utf8.len(State.search_term)
local byte_offset = Text.offset(State.search_term, len)
State.search_term = string.sub(State.search_term, 1, byte_offset-1)
State.search_text = nil
elseif chord == 'down' then
if State.cursor1.pos then
State.cursor1.pos = State.cursor1.pos+1
else
State.cursor1.posB = State.cursor1.posB+1
end
Text.search_next(State)
elseif chord == 'up' then
Text.search_previous(State)
end
return
elseif chord == 'C-f' then
State.search_term = ''
State.search_backup = {
cursor={line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB},
screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB},
}
assert(State.search_text == nil)
-- bifold text
elseif chord == 'C-b' then
State.expanded = not State.expanded
Text.redraw_all(State)
if not State.expanded then
for _,line in ipairs(State.lines) do
line.expanded = nil
end
edit.eradicate_locations_after_the_fold(State)
end
if State.cursor1.posB == nil then
local before = snapshot(State, State.cursor1.line)
if State.lines[State.cursor1.line].dataB == nil then
State.lines[State.cursor1.line].dataB = ''
end
State.lines[State.cursor1.line].expanded = true
State.cursor1.pos = nil
State.cursor1.posB = 1
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
end
schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
end
-- zoom
elseif chord == 'C-=' then
edit.update_font_settings(State, State.font_height+2)
Text.redraw_all(State)
elseif chord == 'C--' then
edit.update_font_settings(State, State.font_height-2)
Text.redraw_all(State)
elseif chord == 'C-0' then
edit.update_font_settings(State, 20)
Text.redraw_all(State)
-- undo
elseif chord == 'C-z' then
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
local event = undo_event(State)
if event then
local src = event.before
State.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 leaks
Text.redraw_all(State)
schedule_save(State)
end
elseif chord == 'C-y' then
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
local event = redo_event(State)
if event then
local src = event.after
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 leaks
Text.redraw_all(State)
schedule_save(State)
end
-- clipboard
elseif chord == 'C-c' then
local s = Text.selection(State)
if s then
App.setClipboardText(s)
end
elseif chord == 'C-x' then
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
local s = Text.cut_selection(State, State.left, State.right)
if s then
App.setClipboardText(s)
end
schedule_save(State)
elseif chord == 'C-v' then
for _,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.line
local before = snapshot(State, before_line)
local clipboard_data = App.getClipboardText()
for _,code in utf8.codes(clipboard_data) do
local c = utf8.char(code)
if c == '\n' then
Text.insert_return(State)
else
Text.insert_at_cursor(State, c)
end
end
if Text.cursor_out_of_screen(State) then
Text.snap_cursor_to_bottom_of_screen(State, State.left, State.right)
end
schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)})
else
for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll
Text.keychord_pressed(State, chord)
end
end
function edit.eradicate_locations_after_the_fold(State)
-- eradicate side B from any locations we track
if State.cursor1.posB then
State.cursor1.posB = nil
State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data)
State.cursor1.pos = Text.pos_at_start_of_screen_line(State, State.cursor1)
end
if State.screen_top1.posB then
State.screen_top1.posB = nil
State.screen_top1.pos = utf8.len(State.lines[State.screen_top1.line].data)
State.screen_top1.pos = Text.pos_at_start_of_screen_line(State, State.screen_top1)
end
end
function edit.key_released(State, key, scancode)
end
function edit.update_font_settings(State, font_height)
State.font_height = font_height
love.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 tests
Test_margin_left = 25
function edit.initialize_test_state()
-- if you change these values, tests will start failing
return edit.initialize_state(
15, -- top margin
Test_margin_left,
App.screen.width, -- right margin = 0
14, -- font height assuming default LÖVE font
15) -- line height
end
-- all textinput events are also keypresses
-- TODO: handle chords of multiple keys
function 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 textinput
function edit.run_after_keychord(State, chord)
edit.keychord_pressed(State, chord)
edit.key_released(State, chord)
App.screen.contents = {}
edit.draw(State)
end
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)
App.screen.contents = {}
edit.draw(State)
end
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)
App.screen.contents = {}
edit.draw(State)
end
function 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' then
load_file_from_source_or_save_directory('icons.lua')
load_file_from_source_or_save_directory('drawing.lua')
load_file_from_source_or_save_directory('geom.lua')
load_file_from_source_or_save_directory('help.lua')
load_file_from_source_or_save_directory('drawing_tests.lua')
else
assert(false, 'unknown app "'..Current_app..'"')
if Current_app == nil or Current_app == 'run' then
App.open_for_reading = function(filename) return io.open(filename, 'r') end
App.open_for_writing = function(filename) return io.open(filename, 'w') end
elseif Current_app == 'source' then
-- HACK: source editor requires a couple of different foundational definitions
App.open_for_reading =
function(filename)
local result = love.filesystem.newFile(filename)
local ok, err = result:open('r')
if ok then
return result
else
return ok, err
end
end
App.open_for_writing =
function(filename)
local result = love.filesystem.newFile(filename)
local ok, err = result:open('w')
if ok then
return result
else
return ok, err
end
end
end