Pensieve.love can now edit itself. This is a large change and possibly destabilizing.
JMUE7GSN6QDQZ6NDRB55MRJMKJN6LBD6MVQPKROYPDOIXM7I3XNQC NGR3TYYJRUHGILU6SWR72R3HBFH6XBJH4ZTT2COES4QNC3EOGLOQC CHHLOX4PVE7S3PDIBCXWHF4CMEWMD5ILXXETPF6MT7MVOD2FN7PAC LUTJO4R2OWGRM5ZYYHUJYIBZZK2UF2VIAE5ZA5IHMPBSDL54OA3AC FHOS6BC7UK4OVBVTR4KIAG3NEB2RNKS7OC5WPOGBXDFKKR4A4UIQC ZMQI7SOJ6VV7PMCMREA2RFXV4SRL5HGH5X3F6HZ6KJ34NFNQACPQC FNPVL6G5I3VYPGSDJYISECSIPLDBG5U3OLRXEMTYH7GNJISXE5WAC PASNDSN4LJPVE6BO7HR6H3NJZPYMQUYKU4Y26I674CNJTW524TNQC 4QOOM6GCO3YKDDE4JVM4ET4HONXH7DX5RCIDBAJSCR6SQLB23KFQC IHCNPJ4DKBWAQJFMOUI6IEIV3KBMUNTSV3L2UWFJIQLH43MOFN3QC FOX735A7FENYEPQ2LSHZ6E2JY7CZRLGRAQJ2I2OHSUZSL2UHCJYAC LFQHNN6VQWZPUNWURISHWEMBHP2WQV6JO6PVKWADXIHFIPACGVEQC SCTWBZBMHEQYSWNUFW2DINOJLNFHUEEWUOIRH7EJOU6FU3CIL7LAC UQU3O7UHMQNRGGKBOIFEEWGYFZMTULGSPJTJLVYBM3JSYY2J5MAAC 4KOALB24GY36M2KJJIEGZ77U4ZS5EXK3JZQ3PKN67WFZH46XIRJQC RCKGEWD6Y7MLTIEFLMC4NBCNKTJNQR2DX3UXOGVJS3MOCFKHYUVAC P62JKMBPFJCGBKXY4TPNN6A7HHIO2DAJAHLGGXUV6YRMD2APKOYQC XZ7HCYMEXWQSCM2SG7U44D6PMTM5VUCMVBWPWTYLEJGLNGSPWJXAC YU2OTMAV4ZZWU5HJFS7TRHBYG3JETOIXZKTKFAGCZSBKFNHF36GQC DIR3ERGQF6UXKE4GCUQZ3T23OREMAXOKAGCEX7UTYLW4NZLJ5BYAC ICKFW4VOTV7BWINRVOYM2ISQOKCVQG4PMMH3QXPM5XHF2Y4ZLSPQC MBFOB7W5FHFWV7UZU62CTCMQ7FJTBYKA7KIVWHISMRWQBI3LR7OQC XFRLE4POTEX7UN2WBWGLDOSLWPWCNSNJZ7LX7LT57HADTW5NCZTQC LHEYSV24NWK4DN7CFXIDGIYPDQVDCQUE2HEE62FUDQEN2KJFORLQC 4D6YLBYHNGAGSEZHNLGYVP62WZC2TRXOO6YVF76DN4KZNBIXGPBQC USHY2DCFJCYQ2ZKI2LHGAKME3ZU4OE7MUMNRKE7G3EPMHRSQ2E6AC A7L3DYU76UKHNXJK7KOIMCDSKGWOQE5UOZHOIXTCUZFNKIMENKDAC YIQFSBC426D4A4Z5XSBAEAEBIVLO5B6HJZZXQY446DFZOAHUFX3QC 3Q6IFM4UFTJYM2D5BK4A54D3N2OMZC33XYPEOB2NEJZ6PDN2WRCAC OOUCNHXBSUYBBBMBWWJZX3CRPXI2KNQWO24UJOO3IYORXK2MLAJQC PRSUR45QDYG7L27BQRCFA2ELO3FSQGOXTTYXSO6UB2JDWWZXPNBQC GAX5GQ6O6CRORVE4NV7SLFEDPLWZAAHUAUQ5LJJOOM7EDHIGZ3OQC DGTRI544ISEG3C2EZIHOHMFV7RN7OPH7PPDES5YKYSPES7GJKTNAC 5AST5334ARX53SIFQG6QCWWYIRO7AHQK5YOB5YKHXRCTN7R73GLAC Y7GV3NKTS5JQQAZ7K2QWU6ZBMZPZMEOX6WRHCOV2LHVZ4FVRXC7QC ZHR7PZSI2O6OGEZUMAVRLIAGOHSC4CNFAXEAFFERQV44VUQ3MQJQC ZPJSLO2XBNFIDZODKEANQGZ6ZUUCNGDOQRVECTUHVIG2QQ3VAQUQC 6HRQ6USPCSQN2XVE3O5ETWOE2I5RNHK5KVRPOARIPNECI2AIICGQC QZ2L5XGJUHVOEM346IBKZSLOJAR2WUAT64R3MAHCID7WNX5RPXXQC XX4O3AFWGSEMXELBKVMS3GSVITFD54BAVEBDILELUQTWGH645C7QC CQYKYJJU3NMY2CNXF7VZWSDYU3SWPFWA3ZHDW24Q4S6QEK5Y2TSAC X3CQLBTR7ICDAVFZZRLAWWJZI2SNZAMRUWW7O4R25DIKDUHL75CQC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC KKMFQDR43ZWVCDRHQLWWX3FCWCFA3ZSXYOBRJNPHUQZR2XPKWULAC 7PZ4CQFVYUMSJKVCNM75VKK5JCUYU6ICHWPZXXIC3S63YJVFCP5QC LXTTOB33N2HCUZFIUDRQGGBVHK2HODRG4NBLH6RXRQZDCHF27BSAC 4CTZOJPCTWYUSHLIZZJ2M5W7S4JZFZVT5MUU5XNSOIBS5L4UY5UQC TVCPXAAU4P3K5MFYINH2MWDK3KGTQ2GE74TUNERYOONG2G5EYKMQC OTIBCAUJ3KDQJLVDN3A536DLZGNRYMGJLORZVR3WLCGXGO6UGO6AC BLWAYPKV3MLDZ4ALXLUJ25AIR6PCIL4RFYNRYLB26GFVC2KQBYBAC UH4YWHW5NDKNR7RS664UG4PRJNZIPNWAD5JWBEUB22JHOY2SWZKAC GQBUV2XOMEPMTXMPCBQWGGIUXGQDX77VTGPFIG6YT7G64ASOYHXQC CE4LZV4TNXJT54CVGM3QANCBP42TMLMZWF2DBSMUYKAHILXIZEMQC UEL3EA2ENBNEUPJUQE4PJYN65R5SOAF7JFRRCBPGQZYWF4KEUIRAC JTDPO5WTXYGCACS6OGKLW4NV2XEDOVZK3UED3FAWCB242ZPGLKWAC V5PFN4BCA6B5PVK2OJ5HFW2AVMYMHQ7HXMVGUWLUQDWAMQFWBWYQC B2QUAQ2ZEM2FSKK5EH4XMMRZQELNWV74UQNVCYPCLFZF7PLL536AC WUTT3JC6LNYV6WD37ZUWSEDO47A7R7TOXUTB4LCGBC4APHUYZO6QC LA63KIU2TQ4ISN3VG3OJKKB6NEKU33RF7MC6XJRJMXEGCBBM4E2QC OFRW3WZELPPJFDDEUVITJKYEBYHCK6HQ3QT2CCYOPSQQZSOC2EKAC CMZFG5EKA2MGFNYA66TJLYQ7VUIXRPCHDBQLZB2QTZMEUMNEGJUAC DGMHQDVOII6WF2OJNHP42AYYMCMAIM266I3KHED74OBYMIWWAQXQC 2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC J3ER7DFO2TXYUMJAXZUFEHQNLFDNIXSYDTE7HEFGQ2RYB3A6RFPAC MO4B3HJQL7KU2CETG74EV367YPREQN3Z5DJP2MNHITVW2KQRXRCAC 23W3KW6BH355C3JYD7LF65FOZENQWWO5PUEAXEBZOV7TMTOQPZQQC 6PUNJS5BSLTYMYMN4JFD7YDEGVQLM5PGAT7PQIG5NIAKLTM5T4PQC 6LJZN727CRPYR34LV75CQF55YZI3E7MGESYZSFSYAE73SNEZE3FAC VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC 3QNOKBFMKBGXBVJIRHR2444JRRMBTABHE4674NR3DT67RRM2X6GAC AD34IX2ZSGYGU3LGY2IZOZNKD4HRQOYJVG5UWMWLXJZJSM62FFOAC IABAN3C47X2R2HJ5H4IYW7FWRZXYFH6NK5UHGGCXS7LHIKCSNNVQC B22JHH4W75CLJTHZWAHJZWYL3L7IN4MSIJSR5CCCCXYWYA27TRNQC QL7T6VAFKSK2AOIIRT3HGDY4UXYFKY747FM4BICGPSKFDUZM62DQC GHJKEJZUVPHDP27CYJQ2RYDUJBF2MM3NHVIALFZGJPHT4PUIOMAQC D7D6T2F3FRMONF627F2NV227T5KTZ4FOHZKROEIIA236U7FVASTQC CZY3IDERLI6MTKKKMX6QLLERSPM2ZJ57NGQRKILJBM7S5PYPQ3XAC DIVBY22FIFTEVZ3TMPJZFTC55G3GU6SBXQ6ILPGNSFPBWKXWGLPQC ZPIIIN2B4EENXZAANI23O2IBKAMFNPLGZYFFMJIZ6ENYVPH5ECDQC 3LOTODIV7AG7WKN3FY7AVJEEOPHVFEEQ7C3WV3PKED23XPO75VEQC W5D22DQ5HRM3SGVEDY3E57Q6UPXGYLPQUJEVYMP2SU5KKG4J5K2AC BRJJ4HZMGVIGRTDGE7ZNRSN3IZIS3A6SL7BFHUV2VVEBT3KIG3RQC X4IMZTJHZDYTKABS2SHGQWP6PN7ESECHYQQKLLGZKBDE7Y4TYX4QC EWK46OTWI2QK7ZZGYOCWWBHXHBXZQWR2FDW6LRTTKCN4K3ZFE5JAC Z575DEB7ULZXYH2BJCFXQMKPVXCSG72P46EHVBDD5PAOQTUDNZHQC YKRUNSPXQMYUZM2HC2BWRL3OLVF5X3Y3XASG7JCT4TW4VUHZD6ZAC 5E6DJTFMGUDYGZ3WT5BGSR7KO5WWZ3LV47HNMTHWVIJLBACTRWVQC 4BX4GJEWW7Z5LA4SJUXADYLAHOYFL4IBOYH4J4DJYRAVKKGGFHGQC PXCTDLE5XS3QSYBVK65TEPA7ZWVZN7GPHKX3ZUEVEO3F6LH3OKSAC UHB4GARJI5AB5UCDCZRFSCJNXGJSLU5DYGUGX5ITYEXI7Q43Z4CAC SYS77RM72TL7SLUQIJBN6D7DI5I6HSHCHXLPXREPADUNBBFM5TYAC TZ3ZNGRMZ3DG33VVEZI74W5W5T73JYMEIYQ3SO6Y4A6FN3OUZIKQC VHWC2IGQD72ZZUIONIAEP45KPMPX2N6DXN6NU2QGBIWDUHP7INAAC EGS44RTAG7JVLLEA4KH63MBZAD4O5HNRSJQIOC6SRKHUG3G56R6QC DUQDA3U7VNWZSKRVACS6G3FTEB5VXRR7FJQU5NYZ4EFSGL3XUU5QC XUU6TARAAE67BNVKF5QLT3J2TKUW3P7ZVY5LESSDJLIRGLFP2U2AC 25GIWMASSPUME4NGCHWBHJDN5FOZVFTWZZFOZ2ZP5YHCKZOY6GJAC 6F3ZAAJHUMKGBRCRG4WMLTDHWHBGY3AXT5WXELIMAA27PQCQVFKQC 2Y7YH7UPQWDNYDJN4BYY2MOHA36B2BIRX6DMIAKHJPQC7UP2R6NQC YGCT2D2ORMLTBHANLGHZV3EBGGHD7ZK55UAM7HF2AVSHDXAAKK5QC N3JOR25T7F4JFEMMSQ7WI4BFY46ESSXVHG24ARGXLASBA43MNSYQC K464QQR4FTXFUMHFWAGOD5DJ6YHUBUKRHLXF2ORE74DVT7TVQ35QC TBRTM3AARI73LHUKOGIT66J5T3UTXQPJFLVJW7ZEBPZCOXAOGIIQC AJB4LFRBMIRBEDWJ3OW7GQIMD2BZBVQ62GH4TE2FISWZKSAHRF4QC R6GUSTBY5ZHR7E46DSIDQDNZDJI6QMZQDC7RPQMQWLGWQKXU6HVQC G3VLJLDHTVRE35JLU3DYRIIHDFE4BAPSMHO3UQZ4W4BDE56LVYVQC U4TATSK6RNCAR3MEBVKIASUJBO6ZI7UJDHYRTJIIXYXB5CLQ7W7AC 5KRLREREOYKEV6DINNF7JSP22AAAUPOEYKVAJ54V5ZESZBNBIHLAC 4VKEE43Z7MUPNIAOCK36INVBNHRTSWRRN37TIKRPXPH3DRKGHHAQC 5MR22SGZE5YDU5CAIY53GNJDA6HSWBPYPD6M3FRQ5ZUMCSKTYJRAC 7AKT7IKOP6VQW3CABAVXCEZTHYNAETVXGFIQWB2L6I7LM7T57HAAC HCLCAFHPRUDGNFOYXKNHME2ELQKSXUTN3E4NV3PT4DGG2VJV7KOAC HQCEHEHJXKH7ZU4OGLHT5QCY75YKPBH6WVSLXJKSQI4V7PKUBVEQC AVTNUQYRBW7IX2YQ3KDLVQ23RGW3BAKTAE7P73ASBYNKOHMQMH5AC 5HOB5Y6ZDNJ42XMHQ7YWZTUCK4DAJRPYRECDWTKFSXZWQ4ZMMSNAC CMGIWHDA5EQY22VK7GJMDMCONMVYKJ7HCB6QISHTMVZGUBIPAYFQC P376DBJTKHBVGQ57PF7LQFZVUMBJPS4QKY2VKVN765FUCIPWL6QAC PH5UM6LIG4QKWG3C7GVHOXHYKDQCAWXBCGKSVSPRKBIYDQGSVGPAC Z4KNS42NJZTQKUQZ7B5NYU2U4VOCUQCBFT2D7423MAXKF7NQ5ZJAC YT5P6TO64XSMCZGTT4SVNFOWUN5ECNXTWCMFXN3YCDZUNH4H3IFAC SHERJHWRESOCZAKG3HCXZCIJIKEMPHLKNPOIMFWJBUEXGQJ52RHAC 3QQZ7W4EJ7G4HQM5IYWXICMAHVRGERY4X6AOC6LOV5NSZ4OBICSAC XH2W55X3V7PQZFIYRQ2ROS6SVAUFCV2ET6TIRACSHTRV2YRXSVIAC KVHUFUFVOSY6GB4XI2QK4T4WCLIYOV3NZR67TX6AQHAQDWJMEOBQC 42BVXPWYBQ7EREUD6OZ56TUGD3R5TKXZVJ3IX46KTYWBJ4YF5SPAC RT6EV6OPUYCXYZOX2PHFXJ7KT77KHNEVINEGQXIQLHQVKPGTN6VQC PX7DDEMOBGPVK3FXKK5XEPG24CJXZSVW67DLG2JZZ5E77NVEAA3AC LNUHQOGHIOFGJXNGA3DZLYEASLYYDGLN2I3EDZY5ANASQAHCG3YQC XX7G2FFJ4QCGQGD4REAW5QFHVYAKCFUPGZCK7L6DFGS5ISVBYBQQC SOWKJG6CUNU7ZON5XWWX7WULX5Q5BU2ZUTQ322KQYPKZVHEXMHCQC AOVGO4PFCRRXU3JPQIVW36UPY6HAGMAH3HHA3E3J7QYZGDYA4B6QC HEL54WE5OI5MS7VXNODYHFIWX4L4DO5CINWRKVBUV3GZ52AVX5KQC UGEB2N5EVBSHMMJNDKYEHFYH2HHXEHJK3UWSXZB4BCVQR2N65OAAC F5FZLXTS63V6GG6RMKSZS3X4BPADVZY436VGVYKHALTIYUB6M65QC LW6UME22NIRDLYNQDP47SLYT2ZEBD4HMHQOJNXJRRXQYVMNF4IGAC U5SQPH4QMBTKIAETAD27Z5CQTFJRRC4ZPOD3AUGMNNYBXWIVOQLQC OPCDU4JRKPTMJXGWCQ3FTYTGQO5CJVSLCOKSDWOFHBFHNQFH4FIQC BVL72CM4WMKHMENSC4JEWEQ5XTB5CXQ5U6BQHTOIL7VJSVZ6DWFAC L32R5DALHPK75E2V7C5QGKR4AH6FVOLGTLMVKVQ36VA5ZKCID2JAC TB2YCOSU2EQBYTKB3GILUYTWZFP4KBADEUTUPAIETMCPGYJMPHCAC 6XOT4ISNFT7DHO76Y4I7ASSQIYIV3DS2K4HV727VCHNCBCLK4SLQC HOOALPX3CGEK6W7K4RRGGHH5GQSFY3VI5T3PQ5UAI67ZNGMEPHTAC FPTS6JMIURRJGEJFPRHOG4HIAASNFN4TOVVOPASCYXRY2V5MZM4QC OSDKHQQ25NJUL2GIVB3LOAYTPA2QPQ6NXTZMW5DIX2LLRLYHD7HAC RTG3J32S5SQX6KC74FFHSK2MAJCMISFUHANSDCIFQA7TGDGDORGAC DRVGMBSRVZ2GH6ZCC4SOPG6RLU53IZ3STZPNDMI6HSUXARY3BZNAC HC2KMIWE2KZFIVUBJPUYYB4ZCMEKBEQIJRPF2SJLNA6ZXGM5R2BAC 3OMHSKUWZAJ7IRQ5HU44H6H345WZZMMKEC5NCALALKMHBEEIIBXQC 7VI7WFVVS3DO7HXXEVT3PQSNIXTD25KLIMKDYEK7E4IYRQ6FKGEAC LFZFMTHXJETFH6TKZ4RRTLHFJZ44HIGIRQEIRVIF37LAVZ74REBAC DVTFQOQRNCSXEOPMYRLUCC5COKFF4ZNUYRBUTZGH5LKCUQUN37SAC VIU2FBNVHG5FV5AJLVPMGEUO5HCLJEGZTRWNY2C5XC4AKMQZZKVAC DYPXQWZ6RE4WQWYRMWLI4JVNTEH637CDVLG5RQ3D5KNPZ73C7UHAC 6LBSEPBJTXIVBYX3AH5FYCQCN66ZRFFO3IVYMDS7IQUGEAZM3MHQC IFGHOCUOIAKC722BNCIRNE2EDMGF77FBTYS57H5UWN6YXOLSZHQQC S4TFD5YYEMH24RVOO5THEOW7WUP4FSICR4UYABSRE2UKXYCNN4DQC UWSWUQZGEXRJ7C6U2XEFFM3RXQIMNLNIEZFLPIPMYP62M44KTBHAC L2R5WULWN44SCNUPP7R7I2AQCWQR6D3E2YZA35RBXZAR2LGC6LQQC PEBXAASMMBD7Q2KFZYEYOVHGHT6LPUHX44JHXNLTPMHMIHSMB6VAC J27QDFGTC3TULGKIMWAX6PHCHPCXCMB46QVRNJ4EVQB6DLKHNPFQC 5U5N3KZY7HZB4YV3UBFO7ZZJLIJUWLKYDSHZXJVWOB36T2M2HWGQC 2UGGJ7JNP6FISC2TGNDP2ZTLVSFS7PUVXRLVENDNSERG6X5G5UNQC 2NC6VRDOYVEVU2LCL4FRZHWWGJ56K6HNRV4HKMNNHLXVVDL65UFAC ZCELTHLQU3DL337ELVAPHLDQ2SFY4ENAQA2STBLLVOLR4PXLKNYQC OYSJONAMU7BURDVV24MXBJHTIFNRRIUWSYG2U4ANMQOGVU26SXLAC QBL4YDQ5QC4MZ7RFS4ZSEBPN5WATAJ7UQLUPTXDUMXAMGBGQDOOQC UBW7MXYYAH32C62YQPT4GAV5KDI4CDJWNYKPQVI24EM7B2SPZMTQC 3JDTNKUEWV3V2ABWCOAR4I5LHY3R4MARCFAAQ3KEYGKYHQOXCC2QC F7JBFZFRALHRUTYYHJ2J7OBBB5WYPJBDTLDLZAMTVC3FUVJ7MAIAC VI2C4J7FIA5M4W37BKXE6IAMGS4BBMOVEPIFFKVHPXESDBAZZKWQC IYW5AHK7E2YFDWTX6OBBLYKNMWFANSPALKGVYXGIBRIRCACVQZEQC L2Z26UKWMGFSOB6NM4BRH4533D5A7CDYEKHKOSWA3Q5BGJWK2QGAC XDOFN6SJR6AXIETUHEIHSBASLH4NKY7PIGNBZ55EWIFNO23IRE7AC SKMUH5RWOBUHRAQ3TUEIH5EEHN3YOC35GJOJY7GPY7VY6QXLSODAC HOCOBJOG6HPMYXPYQRWGVLT3L2ZF6OEKAQSQECRFIG6NVN2DGK2AC FJQM2HAOV7J5263GOEBH4Y55Q2WGKU6ZE5AQ7MMLFHWYAYUP7INQC BENMZANJJ4IGNDJ4D5UIPHP4OWMMKA2ABSZEPO4BPQZT6PKHNR6AC A76AOSKCKOQL5ZGYGWCGNRCMKLUTKGCDVJ5G3ELTIVHZ7OFH35MQC A2NKTJZLXUI7G4QNLGG35D2FL55HBFEJLDMZ62J5IFO3RF6CG2TAC CIOTARCZGYB3CVH6RMR2IGJ7UBKJAFOACPMCIGMHEXRCL63CKR6AC 2RTCG3JZ7G3I3E5N4MX7FZF2IKGS5HFEGNE3E6GWT6BLPINEWSZAC FB7L2QQW6L7X4OWANGKN5U4XFLTJ7G3OINZBQEG3ZT53FUIGKAYAC R6DQAAVQEL7WCE2KTTBGXROZHIBJ5EPP7S4FRPCJT25VCKFOS2WQC BYG5CEMVXANDTBI2ORNVMEY6K3EBRIHZHS4QBK27VONJC5537COQC X22MOJHFLXMZQJN4IP2HAXIIVD2ALPR4EO5V5YDYF6QPXS7ZNB6QC MGH5UZL44BLWWF627W6FZPP5QBPDJ72QVB27GMSRTLTCXHBHPMUAC Y4M5FINMIU5YBMIUAFPNN6FRTONITB4FH4CF4SAN7EZEILT6YRPQC SPTL7VK47SAGTCGQDCQWMEFC43KWJNWCL76NSYHEVBENHIWKRWNAC UHLZULXWCQBLNM5HPCLFGYSIAMWXPXHK6PZCVQHAUQQUF55M6GOAC PLAUXHMHVCNRC4IIAZ2YHFFQWAL5KXC37SMGZJBD4XKSP2PJFSKAC C6J65647YRXQWO2GS3YAXG5R7KT6DVMEC3J3PJV3JWYN32CKAHTQC CUWL2BKJQK75DML6KOE6EGMYUXHSMVSIIKP4SII4NSIOZO2EASGQC TKAKGUAQBDJWK6DPAQC4JJYCAJFVCWNJMKBJIRDH2M3BGEZNSENAC FSB2WPQUW4QF3FYLHKFD6I7XCQXC2PEFI6HITTMGQL4QK4IQ2E4AC 6ES5YSRTVOMR5NJD3S5HEKX57KM66P7JPIIGYZI2NVIUHNGHTNJQC VCX4UWKU3LYXW4LQDJPSHAZ6AJRGS3V27DMRH35IZMMV6FQW4DQAC MEBSNDGI2LWI56TYXWDQCAVNZOKJRQVVIFPNWHSIHK2A2B2QWKSAC NR4MDLT4RL46HGPFOH24XD5TWF4WJIWJZQGQEQAGZ2EUM5C7FUZQC H5JVZ42KOC3QKTARTCJQUZP6473FCWTW72RKPORLARCRVBERBLKQC DQDOVBYXIV26VD3RQYPZNVSIDUPWB56TR737AB3762OXCOXNTZDAC JKAMPNVJHLSYZT5M7GKWGIHTJFIJU3VUJ2AWHLH3ORDUX3UTCF5QC N44GTTQODVIWGKNWMN3TKY5DP7ATK2P6QMZCWRUC3XYGOESV77MAC XKJ5MNAVYTNMQ6EEIH42XQVAGNU2SKBVGJBNCTATDFRWUIOZGFIQC VAMA5WWY2UDCK4UPFY4YV2BM6ETRCQTY6IP3AVKS4L3VHLBYFS2AC NGCYNQEAAROJCQKUJDLGZALNYAU5MTNTGW4XBG6MGAHSHFG6PPMAC BO5P6PG6OYCFBHSKPU3RFQTZRZVEVFAYAWYII4SFNA5ZFI4J7NAQC NMSNEEDD5ENLSFCO4OYYRS5Z5YL2S554TGMI5IU42SIR2OIP6AOAC 3EQZGRICHPHDMY2THLKGTS63G2L3CDED2HILRAY425Z55A26YQXAC 63GT45OBGJFJWASZ3B3HGOUAF6R3H3PBOTAJ2C7PQYZJVI2CZEXQC IK76TR3IFTEH5VUEHUF5KDMIGT3EI5WUXAF2I2SI3OEP3VSLJOXAC VSMPAAJDBVZXZIUFCXY53VM5F63B3ZOS63MCIHH43OA3SKVMJJ7AC FJ4R6IH6EDXQMUYERIB6MFIFSP7QTXLY6EYSSGJP5QIFUV27WY5AC RBFEFVMZHT5O6U2A774HYSDPYRNYDE4RY7RS6OQYKFF4NRTJXT5AC WSFK3NJI3XMC6GGNVTWYFKEPJWT2RWVXSFI3V7L5C36Q43BJGFAQC 46WQF4LQNK54PSZNQLZNYVSDRZU4STNLIGCB4Y7SDWTWQ7ZY7LBQC NAYXSB4JJOE2TQJY4KJ76FICNYUCILJYMOHWXHGFX4MKHGEMP4UAC PYNIWO346ZILMWEIE77CMQDZIMOJ5756VYRCMP6TKWKEWICMTJJQC GFJXAW5RB6TP5YBRDXSEGGJSBZA7YQBBNZERG6L7CRURM66KYHHAC P6TYJLZOXTCK2FZNQFTANG46HBFKFMWUMI6ZU5PRQU3IFWC7WK7AC RQ37HOBMOPOATWRTCEYIRSSGO6P63SQTGFUQM32QFJXKQI5NYDMQC A7CMW3I37OCUUXYPV7M4QRUHQWVEOFLUZ6LOAVKPV47V63QCNCPAC PFD77S6HSVIHU7E76SMCAIODTXL4IOM3K44P2X7SLT3BBIRILXOQC BWZXPP7BDZ5BRYHADWZCK3UELYUL67JO7SDP2QM3YCFSZBWMKYAQC 4A6GJRVUV7PVB62QCNDN7RN6NBHEDN6XPUFJXX22G2OCGNKLCTCAC S4U35JJQWC3CBNKHAEZ2DYEGUT5L3UYVAO6PJ5R7HSTD53KWVAXQC Y3XOLUYVSWJYRIVWLT3SML7GTMJHMWCTHZJF3ZZ5Q5DY4E6VG72AC V6AFS46Z3JN325KKEOZNUJJTCOZOKLWB5O5UUS323VOPGH6J4MXAC YOGSFJHE4VMUGK4NXUIG5HD5MUSSFNKWXH3WDN6UQ5FVXVHLOP5AC R2ZGBQPSLZMAUJLGUYYK77F3RSHOBFBK44IM4LZ4OPNBUOGO27DAC TJQHEGLZEPBQYTOHUBVYWVEJ5ZODGMR24OZKMTUFSXYSTT5L3F7AC BVBSPGDWGKQUDNQW4PGHPETUIAOGPUCB7YINVZTVFE3LSPOJGAHAC UMRNXBPB2VOBLHHWVNFQDF5YYQ3IWYHSGFUTXURKYW4MZLPB33BQC Y7P7UXQQFY7FVORY3ZCGYIQP5QODTVXHJ34N5LB4PII3U63JMWBQC 6H7WAQUD2RDKO5PM2GCHRPNUE76UIT37NQ52ZWVDW46VQGIKHLBQC A7367K2M5DSSDHTOH3J3XYGM6EFQVKAGSN33EJXKOIQSD6GJZQWAC TYIIK36U52MEPMQ52AMVNLYTPUBBHFQD4GHRCFLUYVR7YHYQCY4QC Q7T6TVJJQJKO432SAEMQAMA6Z5NTBFR7JWIR33WM6Q2OQMUBI4WQC XBBYK547GU5XFXPC6KLNBOWX53YHJNNKXUULTOXICAIHRFVMCLUQC EJIR3Y7L2SNZ7YADZZT2YBRIXXJSK4AK7AKPLYIJ7TNISIVO3DWQC UTKVVOY74ICTNL3YNFPPJOJK565BTG673BURU53URE7A24FHQVLQC R6L4RDWKYF7FOWXYKVRCX76GHVH74OQ6AAIZK3YLWDTDQ723FUOAC IOYWCTDEHO4GHC777JYUODJSSHL2SJWYTUG5PQPZOG5HEVR6XXYQC GI22QOP5ATPGEIXBP7CYAMOCFHXTRZQ3AVGNR3KTFIWL2GKBNUHAC HBL3W3PWHKXKCZ6ZI6OZW53M2CMY7N76HCQNGNUDXNO7RYJRJM7QC 6OQPVL7RA6N7G5BM3ZYFOL4MCVPM6S2RGA24LPU2HLVCAAUNGQYAC 5PKJBN4VVCNWTWQAYWXYOHUUUUUENC6SJMFTQI3QFA2BHKS7EQ2AC S2MBCK33FR5HZ52XAJ322XPV4GKRLCNZFW7QKIJUZPONFKAWHONQC APB53SRWHTVVFVFHCPAGYG2F64NL2WGZSAN5CJ3HFP2MFO33GX5AC 66DKCW65WOG7TH44625YEUTUXMGFJ26RDEUVXJE2IFXCG7SVRFSQC 4R6FTCNUKS6IDJLNESSHZNPT5BNNZ64OEL3RBFCCZFZNP5OUSBQAC KO2SK2D4ZSMMYWXCHPMPO236JXTGQEG533GNMWCEVOSQU5XJT2YAC 2FBZUDWUZXGIGJMF6POKYAZL5OGTSTX7PIM2O66WZV2MPIT7ZM5QC S7ZZA3YEKYGLBN6UC2N7WGUS43L6MX2KQQ2LBUZT4FQ7K7V5IQGQC TXDMRA5JEAML2GF5QY4ATU22G3NI7DQWPGO4U5OZNP7NGK4JT6WQC JCSLDGAH2F6AIY4Z6XM6K4LOMW7EFY3E4NF5YXLMHLTYTX3A4Z3QC HRWN5V6J6VMXS7WNSRGI7WMUSZ2OI52JJ4IK352VVSDZI4EF5HHQC MHOUX5JFGBFYMOULX3NZA2JXH6PF2227DT54EEXLBUZQFO7NDI2AC I46AJ2J35CDUUTRGKJAEANZIIOAR63ZKT3LMEPXNLNV54YXL6TNAC UUAE3VQIDTQ6WXHSCB53SXTTZCPOAYHDXJ6OFDLK4IZ6C5MZFMTQC VE7YQMQYCRL77YOFEXFXJXL7VPE6FIEBBJ6KLFZHDWDAKOLMLOUQC HFI2YR2CWHWTAIQMDM6HIHHBUKQ74WA2QXW72PSKZWKHSVFWLKSQC UEE5W7WJ46FIBN4ZH45Z33L4RYXK5AP5ZIBHYTFOJTDWVVX54QKAC FS2ITYYHBLFT66YUC3ENPFYI2HOYHOVEPQIN7NQR6KF5MEK4NKZAC CAVPW3NGOW5IENWZ56A6EZF2FPDE3OP4CORT4RN6SNLMFTBLYZMQC E5FYDACSQNKJG4USM52I6C4KTN3U4Z47C4TK4QYC6RF2FFCZCYCAC D4FEFHQCSILZFQ5VLWNXAIRZNUMCDNGJSM4UJ6T6FDMMIWYRYILQC 25V2GA6JNWMYNBNFLBHFPJ5ZFYQ4E25E4XMTJSTQJGPPK56RSBAAC BJ5X5O4ACBBJ56LRBBSTCW6IBQP4HAEOOOPNH3SKTA4F66YTOIDAC VSBSWTE4IVQDRXLPQ7VTDIIEBEF7GMGRBHZ2IA73ZR6B2KZWI5JAC 5XQ4Y7NU63X2WW4ZR4P46LX5GEOTE7JH3AUMTDQW5VZ53GELNP2QC Menu_background_color = {r=0.6, g=0.8, b=0.6}Menu_border_color = {r=0.6, g=0.7, b=0.6}Menu_command_color = {r=0.2, g=0.2, b=0.2}Menu_highlight_color = {r=0.5, g=0.7, b=0.3}function source.draw_menu_bar()if App.run_tests then return end -- disable in testsApp.color(Menu_background_color)love.graphics.rectangle('fill', 0,0, App.screen.width, Menu_status_bar_height)App.color(Menu_border_color)love.graphics.rectangle('line', 0,0, App.screen.width, Menu_status_bar_height)App.color(Menu_command_color)Menu_cursor = 5if Show_file_navigator thensource.draw_file_navigator()returnendadd_hotkey_to_menu('ctrl+e: run')if Focus == 'edit' thenadd_hotkey_to_menu('ctrl+g: switch file')if Show_log_browser_side thenadd_hotkey_to_menu('ctrl+l: hide log browser')elseadd_hotkey_to_menu('ctrl+l: show log browser')endif Editor_state.expanded thenadd_hotkey_to_menu('ctrl+b: collapse debug prints')elseadd_hotkey_to_menu('ctrl+b: expand debug prints')endadd_hotkey_to_menu('ctrl+d: create/edit debug print')add_hotkey_to_menu('ctrl+f: find in file')add_hotkey_to_menu('alt+left alt+right: prev/next word')elseif Focus == 'log_browser' then-- nothing yetelseassert(false, 'unknown focus "'..Focus..'"')endadd_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')endfunction add_hotkey_to_menu(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endlocal width = App.width(Text_cache[s])if Menu_cursor + width > App.screen.width - 5 thenreturnendApp.color(Menu_command_color)App.screen.draw(Text_cache[s], Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction source.draw_file_navigator()for i,file in ipairs(File_navigation.candidates) doif file == 'source' thenApp.color(Menu_border_color)love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)endadd_file_to_menu(file, i == File_navigation.index)endendfunction add_file_to_menu(s, cursor_highlight)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endlocal width = App.width(Text_cache[s])if Menu_cursor + width > App.screen.width - 5 thenreturnendif cursor_highlight thenApp.color(Menu_highlight_color)love.graphics.rectangle('fill', Menu_cursor-5,5-2, App.width(Text_cache[s])+5*2,Editor_state.line_height+2*2)endApp.color(Menu_command_color)App.screen.draw(Text_cache[s], Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction keychord_pressed_on_file_navigator(chord, key)if chord == 'escape' thenShow_file_navigator = falseelseif chord == 'return' thenlocal candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')source.switch_to_file(candidate)Show_file_navigator = falseelseif chord == 'left' thenif File_navigation.index > 1 thenFile_navigation.index = File_navigation.index-1endelseif chord == 'right' thenif File_navigation.index < #File_navigation.candidates thenFile_navigation.index = File_navigation.index+1endendend
Menu_background_color = {r=0.6, g=0.8, b=0.6}Menu_border_color = {r=0.6, g=0.7, b=0.6}Menu_command_color = {r=0.2, g=0.2, b=0.2}Menu_highlight_color = {r=0.5, g=0.7, b=0.3}function source.draw_menu_bar()if App.run_tests then return end -- disable in testsApp.color(Menu_background_color)love.graphics.rectangle('fill', 0,0, App.screen.width, Menu_status_bar_height)App.color(Menu_border_color)love.graphics.rectangle('line', 0,0, App.screen.width, Menu_status_bar_height)App.color(Menu_command_color)Menu_cursor = 5if Show_file_navigator thensource.draw_file_navigator()returnendadd_hotkey_to_menu('ctrl+u: run')if Focus == 'edit' thenadd_hotkey_to_menu('ctrl+g: switch file')if Show_log_browser_side thenadd_hotkey_to_menu('ctrl+l: hide log browser')elseadd_hotkey_to_menu('ctrl+l: show log browser')endif Editor_state.expanded thenadd_hotkey_to_menu('ctrl+b: collapse debug prints')elseadd_hotkey_to_menu('ctrl+b: expand debug prints')endadd_hotkey_to_menu('ctrl+d: create/edit debug print')add_hotkey_to_menu('ctrl+f: find in file')add_hotkey_to_menu('alt+left alt+right: prev/next word')elseif Focus == 'log_browser' then-- nothing yetelseassert(false, 'unknown focus "'..Focus..'"')endadd_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')endfunction add_hotkey_to_menu(s)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endlocal width = App.width(Text_cache[s])if Menu_cursor + width > App.screen.width - 5 thenreturnendApp.color(Menu_command_color)App.screen.draw(Text_cache[s], Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction source.draw_file_navigator()for i,file in ipairs(File_navigation.candidates) doif file == 'source' thenApp.color(Menu_border_color)love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)endadd_file_to_menu(file, i == File_navigation.index)endendfunction add_file_to_menu(s, cursor_highlight)if Text_cache[s] == nil thenText_cache[s] = App.newText(love.graphics.getFont(), s)endlocal width = App.width(Text_cache[s])if Menu_cursor + width > App.screen.width - 5 thenreturnendif cursor_highlight thenApp.color(Menu_highlight_color)love.graphics.rectangle('fill', Menu_cursor-5,5-2, App.width(Text_cache[s])+5*2,Editor_state.line_height+2*2)endApp.color(Menu_command_color)App.screen.draw(Text_cache[s], Menu_cursor,5)Menu_cursor = Menu_cursor + width + 30endfunction keychord_pressed_on_file_navigator(chord, key)if chord == 'escape' thenShow_file_navigator = falseelseif chord == 'return' thenlocal candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')source.switch_to_file(candidate)Show_file_navigator = falseelseif chord == 'left' thenif File_navigation.index > 1 thenFile_navigation.index = File_navigation.index-1endelseif chord == 'right' thenif File_navigation.index < #File_navigation.candidates thenFile_navigation.index = File_navigation.index+1endendend
Column_header_color = {r=0.7, g=0.7, b=0.7}Pane_title_color = {r=0.5, g=0.5, b=0.5}Pane_title_background_color = {r=0, g=0, b=0, a=0.1}Pane_background_color = {r=0.7, g=0.7, b=0.7, a=0.1}Grab_background_color = {r=0.7, g=0.7, b=0.7}Cursor_pane_background_color = {r=0.7, g=0.7, b=0, a=0.1}Menu_background_color = {r=0.6, g=0.8, b=0.6}Menu_border_color = {r=0.6, g=0.7, b=0.6}Menu_command_color = {r=0.2, g=0.2, b=0.2}Command_palette_background_color = Menu_background_colorCommand_palette_border_color = Menu_border_colorCommand_palette_command_color = Menu_command_colorCommand_palette_alternatives_background_color = Menu_background_colorCommand_palette_highlighted_alternative_background_color = {r=0.5, g=0.7, b=0.3}Command_palette_alternatives_color = {r=0.3, g=0.5, b=0.3}Crosslink_color={r=0, g=0.7, b=0.7}Crosslink_background_color={r=0, g=0, b=0, a=0.1}
-- The note-taking app has a few differences with the baseline editor it's-- forked from:-- - most notes are read-only-- - the editor operates entirely in viewport-relative coordinates; 0,0 is-- the top-left corner of the window. However the note-taking app in-- read-only mode largely operates in absolute coordinates; a potentially-- large 2D space that the window is just a peephole into.---- We'll use the rendering logic in the editor, but only use its event loop-- when a window is being edited (there can only be one all over the entire-- surface)---- Most of the time the viewport affects each pane's top and screen_top. An-- exception is when you're editing a pane and you scroll the cursor inside-- it. In that case we want to affect the viewport (for all panes) based on-- the editable pane's screen_top.
-- tests currently mostly clear their own state
-- stuff we paginate over is organized as follows:-- - there are multiple columns-- - each column contains panes-- - each pane contains editor state as in lines.loveSurface = {}-- The surface may show the same file in multiple panes. This cache tries to-- share data between such aliases:-- line contents when panes are not editable (editable panes can diverge)-- links between files (never in Surface, can never diverge between panes)Cache = {}-- LÖVE renders N frames per second like any game engine, but we don't-- really need that. The only thing that animates in this app is the cursor.---- Until I fix that, the architecture of this app will be to plan what to-- draw only when something changes. That way we minimize the amount of-- computation/power wasted on each of those frames.Panes_to_draw = {} -- array of panes from surfaceColumn_headers_to_draw = {} -- strings with x coordinatesDisplay_settings = {mode='normal',-- valid modes:-- normal (show full surface)-- maximize (show just a single note; focus mode)-- search (notes currently on surface)-- search_all (notes in directory)-- searching_all (search in progress)x=0, y=0, -- <==== Top-left corner of the viewport into the surfacecolumn_width=400,show_palette=false,palette_command='',palette_command_text=App.newText(love.graphics.getFont(), ''),palette_alternative_index=1, palette_candidates=nil,search_term='', search_text=nil,search_backup_x=nil, search_backup_y=nil, search_backup_cursor_pane=nil,search_all_query=nil, search_all_query_text=nil, search_all_terms=nil,search_all_progress_indicator=nil,search_all_pane=nil, search_all_state=nil,}-- display settings that are constantsFont_height = 20Line_height = math.floor(Font_height*1.3)-- space saved for headers-- this is only on the screen, not used on the surface itselfMenu_status_bar_height = 5 + Line_height + 5--? print('menu height', Menu_status_bar_height)Column_header_height = 5 + Line_height + 5--? print('column header height', Column_header_height)Header_height = Menu_status_bar_height + Column_header_height-- padding is the space between panes on the surfacePadding_vertical = 20 -- space between panesPadding_horizontal = 20-- margins are extra space inside the borders of panes on the surfaceMargin_above = 10Margin_below = 10Pan_step = 10Pan = {}Cursor_pane = {col=0, row=1} -- surface column and row index, along with some cached data-- occasional secondary cursorGrab_pane = nil
-- where we store our notes (pane id is also a relative path under there)Directory = 'data/'Settings_file = 'config'-- This little bit of state ensures we don't mess with a pane's screen_top-- if it was just used to update the viewport.Editable_cursor_pane_updated_screen_top = false
if #arg > 0 thenEditor_state.filename = arg[1]load_from_disk(Editor_state)Text.redraw_all(Editor_state)Editor_state.screen_top1 = {line=1, pos=1}Editor_state.cursor1 = {line=1, pos=1}edit.fixup_cursor(Editor_state)
love.window.setTitle('pensieve.love')print('reading notes from '..love.filesystem.getSaveDirectory()..'/'..Directory)print('put any notes there (and make frequent backups)')if love.filesystem.getInfo(Settings_file) thenload_settings()
load_from_disk(Editor_state)Text.redraw_all(Editor_state)if Editor_state.cursor1.line > #Editor_state.lines or Editor_state.lines[Editor_state.cursor1.line].mode ~= 'text' thenedit.fixup_cursor(Editor_state)end
initialize_default_settings()endif Display_settings.column_width > App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal thenDisplay_settings.column_width = math.max(200, App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal)
love.window.setPosition(Settings.x, Settings.y, Settings.displayindex)Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right, Settings.font_height, math.floor(Settings.font_height*1.3))Editor_state.filename = Settings.filenameEditor_state.screen_top1 = Settings.screen_topEditor_state.cursor1 = Settings.cursor
love.window.setPosition(settings.x, settings.y, settings.displayindex)Font_height = settings.font_heightLine_height = math.floor(Font_height*1.3)love.graphics.setFont(love.graphics.newFont(Font_height))Em = App.newText(love.graphics.getFont(), 'm')Display_settings.column_width = settings.column_widthfor _,column_name in ipairs(settings.columns) docreate_column(column_name)endCursor_pane.col = settings.cursor_colCursor_pane.row = settings.cursor_rowDisplay_settings.x = settings.surface_xDisplay_settings.y = settings.surface_y
function run.initialize_default_settings()local font_height = 20love.graphics.setFont(love.graphics.newFont(font_height))local em = App.newText(love.graphics.getFont(), 'm')run.initialize_window_geometry(App.width(em))Editor_state = edit.initialize_state(Margin_top, Margin_left, App.screen.width-Margin_right)Editor_state.font_height = font_heightEditor_state.line_height = math.floor(font_height*1.3)Editor_state.em = emSettings = run.settings()
function initialize_default_settings()initialize_window_geometry()love.graphics.setFont(love.graphics.newFont(Font_height))Em = App.newText(love.graphics.getFont(), 'm')Display_settings.column_width = 40*App.width(Em)-- initialize surface with a single columncommand.recently_modified()
Text.redraw_all(Editor_state)Editor_state.selection1 = {} -- no support for shift drag while we're resizingEditor_state.right = App.screen.width-Margin_rightEditor_state.width = Editor_state.right-Editor_state.leftText.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
--? print('resize:', App.screen.width, App.screen.height)plan_draw()endfunction initialize_cache_if_necessary(id)if Cache[id] then return end--? print('init:', id)Cache[id] = {id=id, filename=Directory..id, left=0, right=Display_settings.column_width, lines={}, line_cache={}}load_from_disk(Cache[id])Cache[id].links = load_links(id)endfunction load_pane(id)--? print('load pane from file', id)initialize_cache_if_necessary(id)local result = edit.initialize_state(0, 0, math.min(Display_settings.column_width, App.screen.width-Margin_right), Font_height, Line_height)result.id = idresult.filename = Directory..idresult.lines = Cache[id].linesresult.line_cache = deepcopy(Cache[id].line_cache) -- should be tiny; deepcopy is just to eliminate any chance of aliasingresult.font_height = Font_heightresult.line_height = Line_heightresult.em = Emresult.editable = falseedit.fixup_cursor(result)return result
function run.filedropped(file)-- first make sure to save edits on any existing fileif Editor_state.next_save thensave_to_disk(Editor_state)
function height(pane)if pane._height == nil thenrefresh_pane_height(pane)endreturn pane._heightend-- keep the structure of this function sync'd with plan_drawfunction refresh_pane_height(pane)--? print('refresh pane height')local y = 0if pane.title theny = y + 5+Line_height+5endfor i=1,#pane.lines dolocal line = pane.lines[i]if pane.line_cache[i] == nil thenpane.line_cache[i] = {}endif line.mode == 'text' thenpane.line_cache[i].fragments = nilpane.line_cache[i].screen_line_starting_pos = nilText.compute_fragments(pane, i)Text.populate_screen_line_starting_pos(pane, i)y = y + Line_height*#pane.line_cache[i].screen_line_starting_posText.clear_screen_line_cache(pane, i)elseif line.mode == 'drawing' then-- nothingy = y + Drawing.pixels(line.h, Display_settings.column_width) + Drawing_padding_heightelseprint(line.mode)assert(false)endendif Cache[pane.id].links and not empty(Cache[pane.id].links) theny = y + 5+Line_height+5 -- for crosslinks
-- clear the slate for the new fileApp.initialize_globals()Editor_state.filename = file:getFilename()file:open('r')Editor_state.lines = load_from_file(file)file:close()Text.redraw_all(Editor_state)edit.fixup_cursor(Editor_state)love.window.setTitle('lines.love - '..Editor_state.filename)
pane._height = yend-- titles are optional and so affect the height of the panefunction add_title(pane, title)pane.title = titlepane._height = nilend-- keep the structure of this function sync'd with refresh_pane_heightfunction plan_draw(options)--? print('update pane bounds')--? print(#Surface, 'columns;', num_panes(), 'panes')Panes_to_draw = {}Column_headers_to_draw = {}local sx = Padding_horizontal + Margin_leftfor column_index, column in ipairs(Surface) doif should_show_column(sx) thentable.insert(Column_headers_to_draw, {name=('%d. %s'):format(column_index, column.name), x = sx-Display_settings.x})local sy = Padding_verticalfor pane_index, pane in ipairs(column) doif sy > Display_settings.y + App.screen.height - Header_height thenbreakend--? print('bounds:', column_index, pane_index, sx,sy)if should_show_pane(pane, sy) thentable.insert(Panes_to_draw, pane)-- stash some short-lived variablespane.column_index = column_indexpane.pane_index = pane_indexlocal y_offset = 0local body_sy = syif column[pane_index].title thenbody_sy = body_sy + 5+Line_height+5endif should_update_screen_top(column_index, pane_index, pane, options) thenif body_sy < Display_settings.y thenpane.screen_top1, y_offset = schema1_of_y(pane, Display_settings.y - body_sy)elsepane.screen_top1 = {line=1, pos=1}endendif body_sy < Display_settings.y thenpane.top = Margin_aboveelsepane.top = body_sy - Display_settings.y + Margin_aboveendpane.top = Header_height + pane.top - y_offset--? print('bounds: =>', pane.top)pane.left = sx - Display_settings.xpane.right = pane.left + Display_settings.column_widthpane.width = pane.right - pane.leftelse-- clear bounds to catch issues earlypane.top = nil--? print('bounds: =>', pane.top)endsy = sy + Margin_above + height(pane) + Margin_below + Padding_verticalendelse-- clear bounds to catch issues earlyfor _, pane in ipairs(column) dopane.top = nilendendsx = sx + Margin_right + Display_settings.column_width + Padding_horizontal + Margin_leftend
function should_update_screen_top(column_index, pane_index, pane, options)if column_index ~= Cursor_pane.col then return true endif pane_index ~= Cursor_pane.row then return true end-- update the cursor pane either if it's not editable, or-- if it was explicitly requestedif not pane.editable then return true endif options == nil then return true endif not options.ignore_editable_cursor_pane then return true endif not Editable_cursor_pane_updated_screen_top then return true endreturn falseend
edit.draw(Editor_state)
--? print(Display_settings.y)if Display_settings.mode == 'normal' thendraw_normal_mode()elseif Display_settings.mode == 'search' thendraw_normal_mode()-- hack: pass in an unexpected object and pun some attributesText.draw_search_bar(Display_settings, --[[force show cursor]] true)elseif Display_settings.mode == 'search_all' thendraw_normal_mode()-- only difference is in command palette belowelseif Display_settings.mode == 'searching_all' thendraw_normal_mode()-- only difference is in command palette belowelseif Display_settings.mode == 'maximize' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenpane.top = Header_height + Margin_abovepane.left = App.screen.width/2 - 20*App.width(Em)pane.right = App.screen.width/2 + 20*App.width(Em)pane.width = pane.right - pane.leftedit.draw(pane)endendelseprint(Display_settings.mode)assert(false)endif Grab_pane thenlocal old_top, old_left, old_right = Grab_pane.top, Grab_pane.left, Grab_pane.rightlocal old_screen_top = Grab_pane.screen_top1Grab_pane.screen_top1 = {line=1, pos=1}Grab_pane.top = App.screen.height - 10*Line_heightGrab_pane.left = App.screen.width - Display_settings.column_width - Margin_right - Padding_horizontalGrab_pane.right = Grab_pane.left + Display_settings.column_widthGrab_pane.width = Grab_pane.right - Grab_pane.leftApp.color(Grab_background_color)love.graphics.rectangle('fill', Grab_pane.left-Margin_left,Grab_pane.top-Margin_above, Grab_pane.width+Margin_left+Margin_right, App.screen.height-Grab_pane.top+Margin_above)edit.draw(Grab_pane)Grab_pane.top, Grab_pane.left, Grab_pane.right = old_top, old_left, old_rightGrab_pane.screen_top1 = old_screen_topenddraw_menu_bar()if Display_settings.mode == 'search_all' or Display_settings.mode == 'searching_all' thendraw_command_palette_for_search_all()elseif Display_settings.show_palette thendraw_command_palette()endendfunction draw_normal_mode()assert(Cursor_pane.col)assert(Cursor_pane.row)--? print('draw', Display_settings.x, Display_settings.y)for _,pane in ipairs(Panes_to_draw) doassert(pane.top)--? if Surface[pane.column_index].name == 'search: donate' then--? print('draw: search: donate', pane, Display_settings.search_all_pane)--? print(#pane.lines, #pane.line_cache, pane._height)--? print(pane.lines[1].data)--? endif pane.title and eq(pane.screen_top1, {line=1, pos=1}) thendraw_title(pane)endedit.draw(pane)if pane_drew_to_bottom(pane) thendraw_links(pane)endif pane.column_index == Cursor_pane.col and pane.pane_index == Cursor_pane.row thenApp.color(Cursor_pane_background_color)if pane.editable and Surface.cursor_on_screen_check thenassert(pane.cursor_y, 'cursor went off screen; this should never happen')Surface.cursor_on_screen_check = falseendelseApp.color(Pane_background_color)endlove.graphics.rectangle('fill', pane.left-Margin_left,pane.top-Margin_above, pane.width+Margin_left+Margin_right, pane.bottom-pane.top+Margin_above+Margin_below)endfor _,header in ipairs(Column_headers_to_draw) do-- column headerApp.color(Column_header_color)love.graphics.rectangle('fill', header.x - Margin_left, Menu_status_bar_height, Margin_left + Display_settings.column_width + Margin_right, Column_header_height)App.color(Text_color)love.graphics.print(header.name, header.x, Menu_status_bar_height+5)endendfunction pane_drew_to_bottom(pane)return pane.bottom < App.screen.height - Line_heightendfunction should_show_column(sx)return overlap(sx-Margin_left, sx+Display_settings.column_width+Margin_right, Display_settings.x, Display_settings.x + App.screen.width)endfunction should_show_pane(pane, sy)return overlap(sy, sy + Margin_above + height(pane) + Margin_below, Display_settings.y, Display_settings.y + App.screen.height - Header_height)endfunction draw_title(pane)assert(pane.title)if Text_cache[pane.title] == nil thenText_cache[pane.title] = App.newText(love.graphics.getFont(), pane.title)endApp.color(Pane_title_color)App.screen.draw(Text_cache[pane.title], pane.left, pane.top-Margin_above -5-Line_height)App.color(Pane_title_background_color)love.graphics.rectangle('fill', pane.left-Margin_left, pane.top-Margin_above-5-Line_height-5, Margin_left+Display_settings.column_width+Margin_right, 5+Line_height+5)endfunction draw_links(pane)local links = Cache[pane.id].linksif links == nil then return endif empty(links) then return endlocal x = pane.leftfor _,label in ipairs(Edge_list) doif Text_cache[label] == nil thenText_cache[label] = App.newText(love.graphics.getFont(), label)endif links[label] thendraw_link(label, x, pane.bottom)endx = x + App.width(Text_cache[label]) + 10 + 10end-- links we don't know about, just in casefor link,_ in pairs(links) doif not Opposite[link] thenif Text_cache[link] == nil thenText_cache[link] = App.newText(love.graphics.getFont(), link)enddraw_link(link, x, pane.bottom)x = x + App.width(Text_cache[link]) + 10 + 10endendpane.bottom = pane.bottom + 5+Line_height+5endfunction draw_link(label, x,y)App.color(Crosslink_color)love.graphics.draw(Text_cache[label], x, y+5)App.color(Crosslink_background_color)love.graphics.rectangle('fill', x-5, y+3, App.width(Text_cache[label])+10, 2+Line_height+2)end-- assumes intervals are half-open: [lo, hi)-- https://en.wikipedia.org/wiki/Interval_(mathematics)function overlap(lo1,hi1, lo2,hi2)-- lo2 hi2-- | |-- | |-- | |if lo1 <= lo2 and hi1 > lo2 thenreturn trueend-- lo2 hi2-- | |-- | |if lo1 < hi2 and hi1 >= hi2 thenreturn trueend-- lo2 hi2-- | |-- | |return lo1 >= lo2 and hi1 <= hi2
edit.update(Editor_state, dt)
if App.mouse_y() < Header_height then-- column headerlove.mouse.setCursor(love.mouse.getSystemCursor('arrow'))elseif in_pane(App.mouse_x(), App.mouse_y()) thenlove.mouse.setCursor(love.mouse.getSystemCursor('arrow'))elselove.mouse.setCursor(love.mouse.getSystemCursor('hand'))endif Pan.x thenDisplay_settings.x = math.max(Pan.x-App.mouse_x(), 0)Display_settings.y = math.max(Pan.y-(App.mouse_y()-Header_height), 0)endif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane and pane.editable thenedit.update(pane, dt)endendif not Display_settings.show_palette and (Display_settings.mode == 'normal' or Display_settings.mode == 'search') and App.mouse_down(1) then-- pan the surface by draggingplan_draw()endif Display_settings.mode == 'searching_all' thenresume_search_all()endendfunction in_pane(x,y)-- duplicate some logic from App.drawlocal sx,sy = to_surface(x,y)local x = Padding_horizontalfor column_idx, column in ipairs(Surface) doif sx < x thenreturn falseendif sx < x + Margin_left + Display_settings.column_width + Margin_right thenlocal y = Padding_verticalfor pane_idx, pane in ipairs(column) doif sy < y thenreturn falseendif sy < y + Margin_above + height(pane) + Margin_below thenreturn trueendy = y + Margin_above + height(pane) + Margin_below + Padding_verticalendendx = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontalendreturn falseendfunction to_pane(sx,sy)-- duplicate some logic from App.drawlocal x = Padding_horizontalfor column_idx, column in ipairs(Surface) doif sx < x thenreturn nilendif sx < x + Margin_left + Display_settings.column_width + Margin_right thenlocal y = Padding_verticalfor pane_idx, pane in ipairs(column) doif sy < y thenreturn nilendif sy < y + Margin_above + height(pane) + Margin_below thenreturn {col=column_idx, row=pane_idx}endy = y + Margin_above + height(pane) + Margin_below + Padding_verticalendendx = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontalendreturn nil
local filename = Editor_state.filenameif filename:sub(1,1) ~= '/' thenfilename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
local column_names = {}for _,column in ipairs(Surface) dotable.insert(column_names, column.name)
font_height=Editor_state.font_height,filename=filename,screen_top=Editor_state.screen_top1, cursor=Editor_state.cursor1
font_height=Font_height,column_width=Display_settings.column_width,surface_x=Display_settings.x,surface_y=Display_settings.y,cursor_col=Cursor_pane.col,cursor_row=Cursor_pane.row,columns=column_names,
return edit.mouse_pressed(Editor_state, x,y, mouse_button)
clear_selections()if Display_settings.mode == 'normal' or Display_settings.mode == 'search' or Display_settings.mode == 'search_all' or Display_settings.mode == 'searching_all' thenmouse_pressed_in_normal_mode(x,y, mouse_button)elseif Display_settings.mode == 'maximize' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenedit.mouse_pressed(pane, x,y, mouse_button)endendelseprint(Display_settings.mode)assert(false)endendfunction clear_selections()for _,column in ipairs(Surface) dofor _,pane in ipairs(column) dopane.selection1 = {}endendendfunction mouse_pressed_in_normal_mode(x,y, mouse_button)Pan = {}if y < Header_height then-- column headers currently not interactablereturnendlocal sx,sy = to_surface(x,y)if in_pane(x,y) then--? print('click on pane')Cursor_pane = to_pane(sx,sy)if Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenedit.mouse_pressed(pane, x,y, mouse_button)pane._height = nilendendelsePan = {x=sx, y=sy}end
return edit.textinput(Editor_state, t)
--? print('textinput', t)-- hotkeys operating on the cursor paneif Display_settings.show_palette thenDisplay_settings.palette_command = Display_settings.palette_command..tDisplay_settings.palette_command_text = App.newText(love.graphics.getFont(), Display_settings.palette_command)Display_settings.palette_alternative_index = 1Display_settings.palette_candidates = candidates()elseif Display_settings.mode == 'normal' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenif not pane.editable then-- global hotkeys for normal modeif t == 'X' thencommand.wider_columns()returnelseif t == 'x' thencommand.narrower_columns()returnend-- send keys to the current paneelseif pane.cursor_x >= 0 and pane.cursor_x < App.screen.width thenif pane.cursor_y >= Header_height and pane.cursor_y < App.screen.height then--? print(('%s typed in editor pane'):format(t))local old_top = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}edit.textinput(pane, t)maybe_update_screen_top_of_cursor_pane(pane, old_top)pane._height = nilplan_draw()endendendendendelseif Display_settings.mode == 'search' then--? print('insert', t)Display_settings.search_term = Display_settings.search_term..tDisplay_settings.search_text = nil-- reset search stateclear_selections()Display_settings.x = Display_settings.search_backup_xDisplay_settings.y = Display_settings.search_backup_yCursor_pane = Display_settings.search_backup_cursor_pane-- search againsearch_next()bring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = trueplan_draw()elseif Display_settings.mode == 'search_all' thenDisplay_settings.search_all_query = Display_settings.search_all_query..tDisplay_settings.search_all_query_text = nilelseif Display_settings.mode == 'searching_all' thenDisplay_settings.mode = 'normal'Display_settings.search_all_query_text = nilelseif Display_settings.mode == 'maximize' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenif pane.editable thenedit.textinput(pane, t)endendendelseprint(Display_settings.mode)assert(false)end
return edit.keychord_pressed(Editor_state, chord, key)
-- global hotkeysif chord == 'C-=' thenupdate_font_settings(Font_height+2)elseif chord == 'C--' thenupdate_font_settings(Font_height-2)elseif chord == 'C-0' thenupdate_font_settings(20)-- mode-specific hotkeyselseif Display_settings.show_palette thenkeychord_pressed_on_command_palette(chord, key)elseif Display_settings.mode == 'normal' thenif chord == 'C-return' thenDisplay_settings.show_palette = trueDisplay_settings.palette_candidates = candidates()elseif chord == 'C-f' thencommand.commence_find_on_surface()elseif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane and pane.editable thenkeychord_pressed_on_editable_pane(pane, chord, key)elsekeychord_pressed_in_normal_mode_with_immutable_pane(pane, chord, key)end-- editable cursor pane will have already updated its screen_top, so don't clobber it hereplan_draw{ignore_editable_cursor_pane=true}endelseif Display_settings.mode == 'search' thenkeychord_pressed_in_search_mode(chord, key)elseif Display_settings.mode == 'search_all' thenkeychord_pressed_in_search_all_mode(chord, key)elseif Display_settings.mode == 'searching_all' theninterrupt_search_all()elseif Display_settings.mode == 'maximize' thenif chord == 'C-return' thenDisplay_settings.show_palette = trueDisplay_settings.palette_candidates = candidates()elsekeychord_pressed_in_maximize_mode(chord, key)endelseprint(Display_settings.mode)assert(false)endendfunction update_font_settings(font_height)local column_width_in_ems = Display_settings.column_width / App.width(Em)Font_height = font_heightlove.graphics.setFont(love.graphics.newFont(Font_height))Line_height = math.floor(font_height*1.3)Em = App.newText(love.graphics.getFont(), 'm')Display_settings.column_width = column_width_in_ems*App.width(Em)for _,column in ipairs(Surface) dofor _,pane in ipairs(column) dopane.font_height = Font_heightpane.line_height = Line_heightpane.em = Empane.left = 0pane.right = Display_settings.column_widthendendclear_all_pane_heights()plan_draw()end-- Scan all panes, while delegating as much work as possible to lines.love search.-- * Text.search_next in lines.love scans from cursor while wrapping around-- within the pane, so we need to work around that.-- * Each pane's search_term field influences whether the search term at-- cursor is highlighted, so we need to manage that as well. At any moment-- we want the search_term and search_text to be set for at most a single-- pane.---- Side-effect: we perturb the cursor of panes as we scan them.function search_next()if Cursor_pane.col < 1 then return endclear_all_search_terms()local pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane == nil thenreturnend--? print('search next', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)local old_cursor_in_cursor_pane = {line=pane.cursor1.line, pos=pane.cursor1.pos}-- scan current pane down from cursorif search_next_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) then--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)returnendpane.cursor1 = old_cursor_in_cursor_pane-- scan current column down from current panefor current_pane_index=Cursor_pane.row+1,#Surface[Cursor_pane.col] dolocal pane = Surface[Cursor_pane.col][current_pane_index]pane.cursor1 = {line=1, pos=1}edit.fixup_cursor(pane)pane.screen_top1 = {line=1, pos=1}if search_next_in_pane(pane) thenCursor_pane.row = current_pane_index--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendendlocal current_column_index = 1 + Cursor_pane.col%#Surface -- (i+1)%#Surface in the presence of 1-indexing-- scan columns past current, looping aroundwhile true dofor current_pane_index,pane in ipairs(Surface[current_column_index]) dopane.cursor1 = {line=1, pos=1}edit.fixup_cursor(pane)pane.screen_top1 = {line=1, pos=1}if search_next_in_pane(pane) thenCursor_pane = {col=current_column_index, row=current_pane_index}--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendend-- loop updatecurrent_column_index = 1 + current_column_index%#Surface -- i = (i+1)%#Surface in the presence of 1-indexing-- termination checkif current_column_index == Cursor_pane.col thenbreakendend-- scan current column until current panefor current_pane_index=1,Cursor_pane.row-1 doif search_next_in_pane(Surface[Cursor_pane.col][current_pane_index]) thenCursor_pane.row = current_pane_index--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendend-- finally, scan the cursor pane until the cursorlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]local old_cursor = pane.cursor1pane.cursor1 = {line=1, pos=1}edit.fixup_cursor(pane)pane.screen_top1 = {line=1, pos=1}if search_next_in_pane(pane) thenif Text.lt1(pane.cursor1, old_cursor) thenreturnendend-- nothing foundpane.cursor1 = old_cursor_in_cursor_paneend-- returns whether it found an occurrencefunction search_next_in_pane(pane)pane.search_term = Display_settings.search_termpane.search_text = Display_settings.search_textpane.search_backup = {cursor={line=pane.cursor1.line, pos=pane.cursor1.pos}, screen_top={line=pane.screen_top1.line, pos=pane.screen_top1.pos}}for i=1,#pane.lines doif pane.line_cache[i] == nil thenpane.line_cache[i] = {}endendif Text.search_next(pane) thenif Text.le1(pane.search_backup.cursor, pane.cursor1) then-- select this occurrencereturn trueend-- Otherwise cursor wrapped around. Skip this pane.end-- Clean up this pane before moving on to the next one.pane.search_term = nilpane.search_text = nilpane.cursor1.line = pane.search_backup.cursor.linepane.cursor1.pos = pane.search_backup.cursor.pospane.screen_top1.line = pane.search_backup.screen_top.linepane.screen_top1.pos = pane.search_backup.screen_top.pospane.search_backup = nilend-- Scan all panes, while delegating as much work as possible to lines.love search.function search_previous()if Cursor_pane.col < 1 then return endclear_all_search_terms()local pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane == nil thenreturnend--? print('search previous', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)local old_cursor_in_cursor_pane = {line=pane.cursor1.line, pos=pane.cursor1.pos}-- scan current pane up from cursorif search_previous_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) then--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)returnendpane.cursor1 = old_cursor_in_cursor_pane-- scan current column down from current panefor current_pane_index=Cursor_pane.row-1,1,-1 dolocal pane = Surface[Cursor_pane.col][current_pane_index]pane.cursor1 = edit.final_cursor(pane)if search_previous_in_pane(pane) thenCursor_pane.row = current_pane_index--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendendlocal current_column_index = 1 + (Cursor_pane.col-2)%#Surface -- (i-1)%#Surface in the presence of 1-indexing-- scan columns past current, looping aroundwhile true dofor current_pane_index = #Surface[current_column_index],1,-1 dolocal pane = Surface[current_column_index][current_pane_index]pane.cursor1 = edit.final_cursor(pane)if search_previous_in_pane(pane) thenCursor_pane = {col=current_column_index, row=current_pane_index}--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendend-- loop updatecurrent_column_index = 1 + (current_column_index-2)%#Surface -- i = (i-1)%#Surface in the presence of 1-indexing-- termination checkif current_column_index == Cursor_pane.col thenbreakendend-- scan current column from bottom current panefor current_pane_index=#Surface[Cursor_pane.col],Cursor_pane.row+1,-1 do--? print('same column', current_pane_index)if search_previous_in_pane(Surface[Cursor_pane.col][current_pane_index]) thenCursor_pane.row = current_pane_index--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendend-- finally, scan the cursor pane from bottom until cursorlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]local old_cursor = pane.cursor1pane.cursor1 = edit.final_cursor(pane)if search_previous_in_pane(pane) thenif Text.lt1(old_cursor, pane.cursor1) thenreturnendend-- nothing foundpane.cursor1 = old_cursor_in_cursor_paneend-- returns whether it found an occurrencefunction search_previous_in_pane(pane)pane.search_term = Display_settings.search_termpane.search_text = Display_settings.search_textpane.search_backup = {cursor={line=pane.cursor1.line, pos=pane.cursor1.pos}, screen_top={line=pane.screen_top1.line, pos=pane.screen_top1.pos}}for i=1,#pane.lines doif pane.line_cache[i] == nil thenpane.line_cache[i] = {}endendif Text.search_previous(pane) thenif Text.lt1(pane.cursor1, pane.search_backup.cursor) then-- select this occurrencereturn trueend-- Otherwise cursor wrapped around. Skip this pane.end-- Clean up this pane before moving on to the previous one.pane.search_term = nilpane.search_text = nilpane.cursor1.line = pane.search_backup.cursor.linepane.cursor1.pos = pane.search_backup.cursor.pospane.screen_top1.line = pane.search_backup.screen_top.linepane.screen_top1.pos = pane.search_backup.screen_top.pospane.search_backup = nilendfunction bring_cursor_of_cursor_pane_in_view(dir)if Cursor_pane.col < 1 thenreturnendlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane == nil thenreturnend--? print('viewport before', Display_settings.x, Display_settings.y)local left_edge_sx = left_edge_sx(Cursor_pane.col)local cursor_sx = left_edge_sx + Text.x_of_schema1(pane, pane.cursor1)local vertically_ok = cursor_sx > Display_settings.x and cursor_sx < Display_settings.x + App.screen.width - App.width(Em)--? print(y_of_schema1(pane, pane.cursor1))--? print('viewport starts at', Display_settings.y)--? print('pane starts at', up_edge_sy(Cursor_pane.col, Cursor_pane.row))--? print('cursor line contains ^'..pane.lines[pane.cursor1.line].data..'$')--? print('cursor is at', y_of_schema1(pane, pane.cursor1), 'from top of pane')local cursor_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.cursor1)--? print('cursor is at', cursor_sy)local horizontally_ok = cursor_sy > Display_settings.y and cursor_sy < Display_settings.y + App.screen.height - Header_height - 2*Line_height -- account for search bar along the bottomif vertically_ok and horizontally_ok thenreturnendif dir == 'up' thenif not vertically_ok thenDisplay_settings.x = left_edge_sx - Margin_left - Padding_horizontalendif not horizontally_ok thenDisplay_settings.y = cursor_sy - 3*Line_heightendelseassert(dir == 'down')if not vertically_ok thenDisplay_settings.x = left_edge_sx + Display_settings.column_width + Margin_right + Padding_horizontal - App.screen.widthendif not horizontally_ok then--? print('cursor used to be at ', cursor_sy - Display_settings.y)--? print('subtract', App.screen.height, App.screen.height-Header_height)Display_settings.y = cursor_sy + Text.search_bar_height(pane) - (App.screen.height - Header_height)-- Bah, temporarily giving up on debugging.Display_settings.y = Display_settings.y + Line_height--? print('=>', Display_settings.y)--? print('cursor now at ', cursor_sy - Display_settings.y)--? print('viewport height', App.screen.height)--? print('cursor row starts', App.screen.height - (cursor_sy-Display_settings.y), 'px above bottom of viewport') -- totally wrongassert(App.screen.height - (cursor_sy-Display_settings.y) > 1.5*Line_height)endend--? print('viewport before clamp', Display_settings.x, Display_settings.y)Display_settings.x = math.max(Display_settings.x, 0)Display_settings.y = math.max(Display_settings.y, 0)--? print('viewport now', Display_settings.x, Display_settings.y)endfunction clear_all_search_terms()for col,column in ipairs(Surface) dofor row,pane in ipairs(column) dopane.search_term = nilpane.search_text = nilendendendfunction keychord_pressed_in_maximize_mode(chord, key)if Cursor_pane.col < 1 thenprint('no current note to edit')returnendlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane == nil thenprint('no current note to edit')returnendif pane.editable thenif chord == 'C-e' thencommand.exit_editing()elseedit.keychord_pressed(pane, chord, key)endelseif chord == 'C-e' thencommand.edit_note()elseif chord == 'C-c' thenedit.keychord_pressed(pane, chord, key)endendendfunction keychord_pressed_on_editable_pane(pane, chord, key)-- ignore if cursor is not visible on screenif pane.cursor_x == nil thenassert(pane.cursor_y == nil)panning_keychord_pressed(chord, key)returnendif chord == 'C-e' thencommand.exit_editing()else--? print(('%s pressed in editor pane'):format(chord))--? print(pane.cursor_x, pane.cursor_y)local old_top = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}edit.keychord_pressed(pane, chord, key)maybe_update_screen_top_of_cursor_pane(pane, old_top)pane._height = nilendendfunction maybe_update_screen_top_of_cursor_pane(pane, old_top)local cursor_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.cursor1)--? print(eq(old_top, pane.screen_top1), eq(old_top, {line=1, pos=1}), pane.top, cursor_sy, cursor_sy - Display_settings.y, App.screen.height - Header_height - Line_height)if not eq(old_top, pane.screen_top1) and eq(old_top, {line=1, pos=1}) and pane.top > Header_height and cursor_sy - Display_settings.y > App.screen.height - Header_height - Line_height then-- pan the surface instead of scrolling within the panepane.screen_top1 = old_topbring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = true -- cursor was on screen before keystroke, so it should remain on screen afterreturnendEditable_cursor_pane_updated_screen_top = not eq(old_top, pane.screen_top1)if Editable_cursor_pane_updated_screen_top then--? print(('screen top changed from (%d,%d) to (%d,%d)'):format(old_top.line, old_top.pos, pane.screen_top1.line, pane.screen_top1.pos))--? print('updating viewport based on screen top')--? print('from', Display_settings.y, y_of_schema1(pane, pane.screen_top1))Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.screen_top1)--? print('to', Display_settings.y)Surface.cursor_on_screen_check = true -- cursor was on screen before keystroke, so it should remain on screen afterendendfunction keychord_pressed_in_normal_mode_with_immutable_pane(pane, chord, key)-- return if no part of cursor pane is visiblelocal left_sx = left_edge_sx(Cursor_pane.col)if not should_show_column(left_sx) thenpanning_keychord_pressed(chord, key)returnendlocal up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)if not should_show_pane(pane, up_sy) thenpanning_keychord_pressed(chord, key)returnendif chord == 'C-e' thencommand.edit_note()elseif chord == 'C-c' thenedit.keychord_pressed(pane, chord, key)elsepanning_keychord_pressed(chord, key)endend-- y offset of a given (line, pos)function y_of_schema1(pane, loc)--? print(('updating viewport y; cursor pane starts at %d; screen top is at %d,%d'):format(result, loc.line, loc.pos))local result = 0if pane.title thenresult = result + 5+Line_height+5endresult = result + Margin_aboveif loc.line == 1 and loc.pos == 1 thenreturn resultendfor i=1,loc.line-1 do--? print('', 'd', i, result)Text.populate_screen_line_starting_pos(pane, i)--? print('', '', #pane.line_cache[i].screen_line_starting_pos, pane.left, pane.right)result = result + line_height(pane, i, pane.left, pane.right)endif pane.lines[loc.line].mode == 'text' thenText.populate_screen_line_starting_pos(pane, loc.line)for i,screen_line_starting_pos in ipairs(pane.line_cache[loc.line].screen_line_starting_pos) doif screen_line_starting_pos >= loc.pos thenbreakendresult = result + Line_heightendend--? print(('viewport at %d'):format(result))return resultendfunction keychord_pressed_in_search_mode(chord, key)if chord == 'escape' thenDisplay_settings.mode = 'normal'clear_all_search_terms()clean_up_panes()-- go back to old viewport--? print('esc; exiting search mode')Display_settings.x = Display_settings.search_backup_xDisplay_settings.y = Display_settings.search_backup_yCursor_pane = Display_settings.search_backup_cursor_pane-- don't forget search textelseif chord == 'return' thenDisplay_settings.mode = 'normal'clear_all_search_terms()clean_up_panes()-- forget old viewport--? print('return; exiting search mode')Display_settings.search_backup_x = nilDisplay_settings.search_backup_y = nilDisplay_settings.search_backup_cursor_pane = nil-- don't forget search textelseif chord == 'backspace' thenlocal len = utf8.len(Display_settings.search_term)local byte_offset = Text.offset(Display_settings.search_term, len)Display_settings.search_term = string.sub(Display_settings.search_term, 1, byte_offset-1)Display_settings.search_text = nil-- reset search stateclear_selections()Display_settings.x = Display_settings.search_backup_xDisplay_settings.y = Display_settings.search_backup_yCursor_pane = Display_settings.search_backup_cursor_pane-- search againsearch_next()bring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = trueplan_draw()--? print('backspace; search term is now', Display_settings.search_term)elseif chord == 'C-v' thenDisplay_settings.search_term = Display_settings.search_term..App.getClipboardText()Display_settings.search_text = nil-- reset search stateclear_selections()Display_settings.x = Display_settings.search_backup_xDisplay_settings.y = Display_settings.search_backup_yCursor_pane = Display_settings.search_backup_cursor_pane-- search againsearch_next()bring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = trueplan_draw()--? print('paste; search term is now', Display_settings.search_term)elseif chord == 'up' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thensearch_previous()bring_cursor_of_cursor_pane_in_view('up')Surface.cursor_on_screen_check = trueplan_draw()endendelseif chord == 'down' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenpane.cursor1.pos = pane.cursor1.pos+1search_next()bring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = trueplan_draw()endend-- things from normal mode we still wantelseif chord == 'C-c' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenedit.keychord_pressed(pane, chord, key)endendendendfunction keychord_pressed_in_search_all_mode(chord, key)if chord == 'escape' thenDisplay_settings.mode = 'normal'-- don't forget search textDisplay_settings.search_all_state = nilelseif chord == 'return' thenfinalize_search_all_pane()add_search_all_pane_to_right_of_cursor()Display_settings.mode = 'searching_all'plan_draw()elseif chord == 'backspace' thenlocal len = utf8.len(Display_settings.search_all_query)local byte_offset = Text.offset(Display_settings.search_all_query, len)Display_settings.search_all_query = string.sub(Display_settings.search_all_query, 1, byte_offset-1)Display_settings.search_all_query_text = nil--? print('backspace; search_all term is now', Display_settings.search_all_query)elseif chord == 'C-v' thenDisplay_settings.search_all_query = Display_settings.search_all_query..App.getClipboardText()Display_settings.search_all_query_text = nil--? print('paste; search_all term is now', Display_settings.search_all_query)endend-- return (line, pos) of the screen line starting near a given y offset, and-- y_offset remaining after the calculation-- invariants:-- - 0 <= y_offset <= Line_height if line is text-- - let loc, y_offset = schema1_of_y(pane, y)-- y - y_offset == y_of_schema1(pane, loc)function schema1_of_y(pane, y)assert(y >= 0)local y_offset = yfor i=1,#pane.lines do--? print('--', y_offset)Text.populate_screen_line_starting_pos(pane, i)local height = line_height(pane, i, pane.left, pane.right)if y_offset < height thenlocal line = pane.lines[i]if line.mode ~= 'text' thenreturn {line=i, pos=1}, y_offsetelselocal nlines = math.floor(y_offset/pane.line_height)--? print(y_offset, pane.line_height, nlines)assert(nlines >= 0 and nlines < #pane.line_cache[i].screen_line_starting_pos)local pos = pane.line_cache[i].screen_line_starting_pos[nlines+1] -- switch to 1-indexingy_offset = y_offset - nlines*pane.line_heightreturn {line=i, pos=pos}, y_offsetendendy_offset = y_offset - heightend-- y is below the panereturn {line=#pane.lines+1, pos=1}, y_offsetendfunction line_height(State, line_index, left, right)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line.mode == 'text' thenreturn Line_height*#line_cache.screen_line_starting_poselsereturn Drawing.pixels(line.h, right-left) + Drawing_padding_heightendendfunction stop_editing_all()local edit_count = 0for _,column in ipairs(Surface) dofor _,pane in ipairs(column) doif pane.editable thenstop_editing(pane)edit_count = edit_count+1endendendassert(edit_count <= 1)endfunction stop_editing(pane)edit.quit(pane)-- save symmetric linksfor rel,target in pairs(Cache[pane.id].links) doinitialize_cache_if_necessary(target)save_links(target)endif Display_settings.mode ~= 'maximize' thenrefresh_panes(pane)endpane.editable = falseendfunction panning_keychord_pressed(chord, key)if chord == 'up' thenDisplay_settings.y = math.max(Display_settings.y - Pan_step, 0)local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)local up_py = up_sy - Display_settings.yif up_py > 2/3*App.screen.height thenCursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))endelseif chord == 'down' thenlocal visible_column_max_y = most(column_height, visible_columns())if visible_column_max_y - Display_settings.y > App.screen.height/2 thenDisplay_settings.y = Display_settings.y + Pan_stependlocal down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)local down_px = down_sx - Display_settings.yif down_px < App.screen.height/3 thenCursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))endelseif chord == 'left' thenDisplay_settings.x = math.max(Display_settings.x - Pan_step, 0)local left_sx = left_edge_sx(Cursor_pane.col)local left_px = left_sx - Display_settings.xif left_px > App.screen.width - Margin_right - Display_settings.column_width/2 thenCursor_pane.col = math.min(#Surface, col(Display_settings.x + App.screen.width - Margin_right - Display_settings.column_width/2))Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)endelseif chord == 'right' thenif Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) thenDisplay_settings.x = Display_settings.x + Pan_stependlocal right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_widthlocal right_px = right_sx - Display_settings.xif right_px < Margin_left + Display_settings.column_width/2 thenCursor_pane.col = math.min(#Surface, col(Display_settings.x + Margin_left + Display_settings.column_width/2))Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)endelseif chord == 'pageup' or chord == 'S-up' thenDisplay_settings.y = math.max(Display_settings.y - App.screen.height + Line_height*2, 0)local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)local up_py = up_sy - Display_settings.yif up_py > 2/3*App.screen.height thenCursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))endelseif chord == 'pagedown' or chord == 'S-down' then--? print('pagedown')local visible_column_max_y = most(column_height, visible_columns())if visible_column_max_y - Display_settings.y > App.screen.height then--? print('updating viewport')Display_settings.y = Display_settings.y + App.screen.height - Line_height*2endlocal down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)local down_px = down_sx - Display_settings.yif down_px < App.screen.height/3 then--? print('updating row')Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))--? print('=>', Cursor_pane.row)endelseif chord == 'S-left' thenDisplay_settings.x = math.max(Display_settings.x - Margin_left - Display_settings.column_width - Margin_right - Padding_horizontal, 0)local left_sx = left_edge_sx(Cursor_pane.col)local left_px = left_sx - Display_settings.xif left_px > App.screen.width - Margin_right - Display_settings.column_width/2 thenCursor_pane.col = math.min(#Surface, col(Display_settings.x + App.screen.width - Margin_right - Display_settings.column_width/2))Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)endelseif chord == 'S-right' thenif Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) thenDisplay_settings.x = Display_settings.x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontallocal right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_widthlocal right_px = right_sx - Display_settings.xif right_px < Margin_left + Display_settings.column_width/2 thenCursor_pane.col = math.min(#Surface, col(Display_settings.x + Margin_left + Display_settings.column_width/2))Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)endendelseif chord == 'C-down' thencommand.down_one_pane()elseif chord == 'C-up' thencommand.up_one_pane()elseif chord == 'C-end' thencommand.bottom_pane_of_column()elseif chord == 'C-home' thencommand.top_pane_of_column()end--? print('after', Cursor_pane.col, Cursor_pane.row)endfunction visible_columns()local result = {}local col = col(Display_settings.x)local x = left_edge_sx(col) - Display_settings.xwhile col <= #Surface dox = x + Padding_horizontaltable.insert(result, col)x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontalif x > App.screen.width thenbreakendcol = col+1endreturn resultendfunction refresh_panes(pane)--? print('refreshing')Cache[pane.id].lines = pane.linesfor x,col in ipairs(Surface) dofor y,p in ipairs(col) doif p.id == pane.id then--? print(x,y)p.lines = pane.linesp._height = nilText.redraw_all(p)endendendplan_draw()
return edit.key_released(Editor_state, key, scancode)
if Cursor_pane.col < 1 thenreturnendlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane and pane.editable thenedit.key_released(pane, key, scancode)endendfunction clear_all_pane_heights()Text_cache = {}for _,column in ipairs(Surface) dofor _,pane in ipairs(column) dopane._height = nilendendend-- convert x surface pixel coordinate into column indexfunction col(x)return 1 + math.floor(x / (Padding_horizontal + Display_settings.column_width))end-- col is 1-indexed-- returns x surface pixel coordinate of left edge of column colfunction left_edge_sx(col)return (col-1)*(Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) + Padding_horizontal + Margin_leftendfunction row(col, y)local sy = Padding_verticalfor i,pane in ipairs(Surface[col]) do--? print('', i, y, sy, next_sy)local next_sy = sy + Margin_above + height(pane) + Margin_below + Padding_verticalif next_sy > y thenreturn iendsy = next_syendreturn #Surface[col]endfunction up_edge_sy(col, row)local result = Padding_verticalfor i=1,row-1 dolocal pane = Surface[col][i]result = result + Margin_above + height(pane) + Margin_below + Padding_verticalendreturn resultendfunction down_edge_sx(col, row)local result = Padding_verticalfor i=1,row dolocal pane = Surface[col][i]result = result + Margin_above + height(pane) + Margin_below + Padding_verticalendreturn result - Padding_verticalendfunction column_height(col)local result = Padding_verticalfor pane_index, pane in ipairs(Surface[col]) doresult = result + Margin_above + height(pane) + Margin_below + Padding_verticalendreturn resultendfunction most(f, arr)local result = nilfor _,x in ipairs(arr) dolocal curr = f(x)if result == nil or result < curr thenresult = currendendreturn result
Column_header_color = {r=0.7, g=0.7, b=0.7}Pane_title_color = {r=0.5, g=0.5, b=0.5}Pane_title_background_color = {r=0, g=0, b=0, a=0.1}Pane_background_color = {r=0.7, g=0.7, b=0.7, a=0.1}Grab_background_color = {r=0.7, g=0.7, b=0.7}Cursor_pane_background_color = {r=0.7, g=0.7, b=0, a=0.1}Menu_background_color = {r=0.6, g=0.8, b=0.6}Menu_border_color = {r=0.6, g=0.7, b=0.6}Menu_command_color = {r=0.2, g=0.2, b=0.2}Command_palette_background_color = Menu_background_colorCommand_palette_border_color = Menu_border_colorCommand_palette_command_color = Menu_command_colorCommand_palette_alternatives_background_color = Menu_background_colorCommand_palette_highlighted_alternative_background_color = {r=0.5, g=0.7, b=0.3}Command_palette_alternatives_color = {r=0.3, g=0.5, b=0.3}Crosslink_color={r=0, g=0.7, b=0.7}Crosslink_background_color={r=0, g=0, b=0, a=0.1}-- The note-taking app has a few differences with the baseline editor it's-- forked from:-- - most notes are read-only-- - the editor operates entirely in viewport-relative coordinates; 0,0 is-- the top-left corner of the window. However the note-taking app in-- read-only mode largely operates in absolute coordinates; a potentially-- large 2D space that the window is just a peephole into.---- We'll use the rendering logic in the editor, but only use its event loop-- when a window is being edited (there can only be one all over the entire-- surface)---- Most of the time the viewport affects each pane's top and screen_top. An-- exception is when you're editing a pane and you scroll the cursor inside-- it. In that case we want to affect the viewport (for all panes) based on-- the editable pane's screen_top.
-- stuff we paginate over is organized as follows:-- - there are multiple columns-- - each column contains panes-- - each pane contains editor state as in lines.loveSurface = {}-- The surface may show the same file in multiple panes. This cache tries to-- share data between such aliases:-- line contents when panes are not editable (editable panes can diverge)-- links between files (never in Surface, can never diverge between panes)Cache = {}-- LÖVE renders N frames per second like any game engine, but we don't-- really need that. The only thing that animates in this app is the cursor.---- Until I fix that, the architecture of this app will be to plan what to-- draw only when something changes. That way we minimize the amount of-- computation/power wasted on each of those frames.Panes_to_draw = {} -- array of panes from surfaceColumn_headers_to_draw = {} -- strings with x coordinates
Display_settings = {mode='normal',-- valid modes:-- normal (show full surface)-- maximize (show just a single note; focus mode)-- search (notes currently on surface)-- search_all (notes in directory)-- searching_all (search in progress)x=0, y=0, -- <==== Top-left corner of the viewport into the surfacecolumn_width=400,show_palette=false,palette_command='',palette_command_text=App.newText(love.graphics.getFont(), ''),palette_alternative_index=1, palette_candidates=nil,search_term='', search_text=nil,search_backup_x=nil, search_backup_y=nil, search_backup_cursor_pane=nil,search_all_query=nil, search_all_query_text=nil, search_all_terms=nil,search_all_progress_indicator=nil,search_all_pane=nil, search_all_state=nil,}-- display settings that are constantsFont_height = 20Line_height = math.floor(Font_height*1.3)-- space saved for headers-- this is only on the screen, not used on the surface itselfMenu_status_bar_height = 5 + Line_height + 5--? print('menu height', Menu_status_bar_height)Column_header_height = 5 + Line_height + 5--? print('column header height', Column_header_height)Header_height = Menu_status_bar_height + Column_header_height-- padding is the space between panes on the surfacePadding_vertical = 20 -- space between panesPadding_horizontal = 20-- margins are extra space inside the borders of panes on the surfaceMargin_above = 10Margin_below = 10Pan_step = 10Pan = {}Cursor_pane = {col=0, row=1} -- surface column and row index, along with some cached data-- occasional secondary cursorGrab_pane = nil-- where we store our notes (pane id is also a relative path under there)Directory = 'data/'Settings_file = 'config'-- This little bit of state ensures we don't mess with a pane's screen_top-- if it was just used to update the viewport.Editable_cursor_pane_updated_screen_top = false-- a few text objects we can avoid recomputing unless the font changesText_cache = {}
assert(#arg <= 1)if #arg > 0 thenDirectory = 'data.'..arg[1]..'/'Settings_file = 'config.'..arg[1]
if Current_app == 'run' thenload_file_from_source_or_save_directory('file.lua')load_file_from_source_or_save_directory('run.lua')load_file_from_source_or_save_directory('commands.lua')load_file_from_source_or_save_directory('edit.lua')load_file_from_source_or_save_directory('text.lua')load_file_from_source_or_save_directory('search.lua')load_file_from_source_or_save_directory('select.lua')load_file_from_source_or_save_directory('undo.lua')load_file_from_source_or_save_directory('icons.lua')load_file_from_source_or_save_directory('text_tests.lua')load_file_from_source_or_save_directory('run_tests.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')elseload_file_from_source_or_save_directory('source_file.lua')load_file_from_source_or_save_directory('source.lua')load_file_from_source_or_save_directory('source_commands.lua')load_file_from_source_or_save_directory('source_edit.lua')load_file_from_source_or_save_directory('log_browser.lua')load_file_from_source_or_save_directory('source_text.lua')load_file_from_source_or_save_directory('search.lua')load_file_from_source_or_save_directory('select.lua')load_file_from_source_or_save_directory('source_undo.lua')load_file_from_source_or_save_directory('colorize.lua')load_file_from_source_or_save_directory('source_text_tests.lua')load_file_from_source_or_save_directory('source_tests.lua')
love.window.setTitle('pensieve.love')print('reading notes from '..love.filesystem.getSaveDirectory()..'/'..Directory)print('put any notes there (and make frequent backups)')-- but some files we want to only load sometimesfunction App.load()if love.filesystem.getInfo(Settings_file) thenload_settings()
function App.initialize_globals()if Current_app == 'run' thenrun.initialize_globals()elseif Current_app == 'source' thensource.initialize_globals()
if Display_settings.column_width > App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal thenDisplay_settings.column_width = math.max(200, App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal)
-- for hysteresis in a few placesLast_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700Last_resize_time = App.getTime()endfunction App.initialize(arg)if Current_app == 'run' thenrun.initialize(arg)elseif Current_app == 'source' thensource.initialize(arg)elseassert(false, 'unknown app "'..Current_app..'"')
Settings = json.decode(love.filesystem.read('config'))Current_app = Settings.current_appif Current_app == nil thenCurrent_app = 'run'
function App.resize(w, h)App.screen.width, App.screen.height = w, h--? print('resize:', App.screen.width, App.screen.height)Last_resize_time = App.getTime()plan_draw()
function App.resize(w,h)if Current_app == 'run' thenif run.resize then run.resize(w,h) endelseif Current_app == 'source' thenif source.resize then source.resize(w,h) endelseassert(false, 'unknown app "'..Current_app..'"')endLast_resize_time = App.getTime()function App.filedropped(file)if Current_app == 'run' thenif run.filedropped then run.filedropped(file) endelseif Current_app == 'source' thenif source.filedropped then source.filedropped(file) endelseassert(false, 'unknown app "'..Current_app..'"')end
function initialize_cache_if_necessary(id)if Cache[id] then return end--? print('init:', id)Cache[id] = {id=id, filename=Directory..id, left=0, right=Display_settings.column_width, lines={}, line_cache={}}load_from_disk(Cache[id])Cache[id].links = load_links(id)
function App.focus(in_focus)if in_focus thenLast_focus_time = App.getTime()if Current_app == 'run' thenif run.focus then run.focus(in_focus) endelseif Current_app == 'source' thenif source.focus then source.focus(in_focus) endelseassert(false, 'unknown app "'..Current_app..'"')end--if Current_app == 'run' thenrun.update(dt)elseif Current_app == 'source' thensource.update(dt)elseassert(false, 'unknown app "'..Current_app..'"')
endfunction App.draw()if Current_app == 'run' thenrun.draw()elseif Current_app == 'source' thensource.draw()elseassert(false, 'unknown app "'..Current_app..'"')endendfunction App.update(dt)-- some hysteresis while resizingif App.getTime() < Last_resize_time + 0.1 thenreturnend
endendfunction load_pane(id)--? print('load pane from file', id)initialize_cache_if_necessary(id)local result = edit.initialize_state(0, 0, math.min(Display_settings.column_width, App.screen.width-Margin_right), Font_height, Line_height)result.id = idresult.filename = Directory..idresult.lines = Cache[id].linesresult.line_cache = deepcopy(Cache[id].line_cache) -- should be tiny; deepcopy is just to eliminate any chance of aliasingresult.font_height = Font_heightresult.line_height = Line_heightresult.em = Emresult.editable = falseedit.fixup_cursor(result)return resultendfunction height(pane)if pane._height == nil thenrefresh_pane_height(pane)
-- keep the structure of this function sync'd with plan_drawfunction refresh_pane_height(pane)--? print('refresh pane height')local y = 0if pane.title theny = y + 5+Line_height+5endfor i=1,#pane.lines dolocal line = pane.lines[i]if pane.line_cache[i] == nil thenpane.line_cache[i] = {}endif line.mode == 'text' thenpane.line_cache[i].fragments = nilpane.line_cache[i].screen_line_starting_pos = nilText.compute_fragments(pane, i)Text.populate_screen_line_starting_pos(pane, i)y = y + Line_height*#pane.line_cache[i].screen_line_starting_posText.clear_screen_line_cache(pane, i)elseif line.mode == 'drawing' then-- nothingy = y + Drawing.pixels(line.h, Display_settings.column_width) + Drawing_padding_heightelseprint(line.mode)assert(false)endendif Cache[pane.id].links and not empty(Cache[pane.id].links) theny = y + 5+Line_height+5 -- for crosslinksendpane._height = y
-- titles are optional and so affect the height of the panefunction add_title(pane, title)pane.title = titlepane._height = nilend-- keep the structure of this function sync'd with refresh_pane_heightfunction plan_draw(options)--? print('update pane bounds')--? print(#Surface, 'columns;', num_panes(), 'panes')Panes_to_draw = {}Column_headers_to_draw = {}local sx = Padding_horizontal + Margin_leftfor column_index, column in ipairs(Surface) doif should_show_column(sx) thentable.insert(Column_headers_to_draw, {name=('%d. %s'):format(column_index, column.name), x = sx-Display_settings.x})local sy = Padding_verticalfor pane_index, pane in ipairs(column) doif sy > Display_settings.y + App.screen.height - Header_height thenbreakend--? print('bounds:', column_index, pane_index, sx,sy)if should_show_pane(pane, sy) thentable.insert(Panes_to_draw, pane)-- stash some short-lived variablespane.column_index = column_indexpane.pane_index = pane_indexlocal y_offset = 0local body_sy = syif column[pane_index].title thenbody_sy = body_sy + 5+Line_height+5endif should_update_screen_top(column_index, pane_index, pane, options) thenif body_sy < Display_settings.y thenpane.screen_top1, y_offset = schema1_of_y(pane, Display_settings.y - body_sy)elsepane.screen_top1 = {line=1, pos=1}endendif body_sy < Display_settings.y thenpane.top = Margin_aboveelsepane.top = body_sy - Display_settings.y + Margin_aboveendpane.top = Header_height + pane.top - y_offset--? print('bounds: =>', pane.top)pane.left = sx - Display_settings.xpane.right = pane.left + Display_settings.column_widthpane.width = pane.right - pane.leftelse-- clear bounds to catch issues earlypane.top = nil--? print('bounds: =>', pane.top)endsy = sy + Margin_above + height(pane) + Margin_below + Padding_verticalendelse-- clear bounds to catch issues earlyfor _, pane in ipairs(column) dopane.top = nilendendsx = sx + Margin_right + Display_settings.column_width + Padding_horizontal + Margin_left
endfunction should_update_screen_top(column_index, pane_index, pane, options)if column_index ~= Cursor_pane.col then return true endif pane_index ~= Cursor_pane.row then return true end-- update the cursor pane either if it's not editable, or-- if it was explicitly requestedif not pane.editable then return true endif options == nil then return true endif not options.ignore_editable_cursor_pane then return true endif not Editable_cursor_pane_updated_screen_top then return true endreturn falseendfunction App.draw()--? print(Display_settings.y)if Display_settings.mode == 'normal' thendraw_normal_mode()elseif Display_settings.mode == 'search' thendraw_normal_mode()-- hack: pass in an unexpected object and pun some attributesText.draw_search_bar(Display_settings, --[[force show cursor]] true)elseif Display_settings.mode == 'search_all' thendraw_normal_mode()-- only difference is in command palette belowelseif Display_settings.mode == 'searching_all' thendraw_normal_mode()-- only difference is in command palette belowelseif Display_settings.mode == 'maximize' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenpane.top = Header_height + Margin_abovepane.left = App.screen.width/2 - 20*App.width(Em)pane.right = App.screen.width/2 + 20*App.width(Em)pane.width = pane.right - pane.leftedit.draw(pane)endendelseprint(Display_settings.mode)assert(false)endif Grab_pane thenlocal old_top, old_left, old_right = Grab_pane.top, Grab_pane.left, Grab_pane.rightlocal old_screen_top = Grab_pane.screen_top1Grab_pane.screen_top1 = {line=1, pos=1}Grab_pane.top = App.screen.height - 10*Line_heightGrab_pane.left = App.screen.width - Display_settings.column_width - Margin_right - Padding_horizontalGrab_pane.right = Grab_pane.left + Display_settings.column_widthGrab_pane.width = Grab_pane.right - Grab_pane.leftApp.color(Grab_background_color)love.graphics.rectangle('fill', Grab_pane.left-Margin_left,Grab_pane.top-Margin_above, Grab_pane.width+Margin_left+Margin_right, App.screen.height-Grab_pane.top+Margin_above)edit.draw(Grab_pane)Grab_pane.top, Grab_pane.left, Grab_pane.right = old_top, old_left, old_rightGrab_pane.screen_top1 = old_screen_topenddraw_menu_bar()if Display_settings.mode == 'search_all' or Display_settings.mode == 'searching_all' thendraw_command_palette_for_search_all()elseif Display_settings.show_palette thendraw_command_palette()endendfunction draw_normal_mode()assert(Cursor_pane.col)assert(Cursor_pane.row)--? print('draw', Display_settings.x, Display_settings.y)for _,pane in ipairs(Panes_to_draw) doassert(pane.top)--? if Surface[pane.column_index].name == 'search: donate' then--? print('draw: search: donate', pane, Display_settings.search_all_pane)--? print(#pane.lines, #pane.line_cache, pane._height)--? print(pane.lines[1].data)--? endif pane.title and eq(pane.screen_top1, {line=1, pos=1}) thendraw_title(pane)endedit.draw(pane)if pane_drew_to_bottom(pane) thendraw_links(pane)endif pane.column_index == Cursor_pane.col and pane.pane_index == Cursor_pane.row thenApp.color(Cursor_pane_background_color)if pane.editable and Surface.cursor_on_screen_check thenassert(pane.cursor_y, 'cursor went off screen; this should never happen')Surface.cursor_on_screen_check = falseendelseApp.color(Pane_background_color)endlove.graphics.rectangle('fill', pane.left-Margin_left,pane.top-Margin_above, pane.width+Margin_left+Margin_right, pane.bottom-pane.top+Margin_above+Margin_below)endfor _,header in ipairs(Column_headers_to_draw) do-- column headerApp.color(Column_header_color)love.graphics.rectangle('fill', header.x - Margin_left, Menu_status_bar_height, Margin_left + Display_settings.column_width + Margin_right, Column_header_height)App.color(Text_color)love.graphics.print(header.name, header.x, Menu_status_bar_height+5)endendfunction pane_drew_to_bottom(pane)return pane.bottom < App.screen.height - Line_heightendfunction should_show_column(sx)return overlap(sx-Margin_left, sx+Display_settings.column_width+Margin_right, Display_settings.x, Display_settings.x + App.screen.width)endfunction should_show_pane(pane, sy)return overlap(sy, sy + Margin_above + height(pane) + Margin_below, Display_settings.y, Display_settings.y + App.screen.height - Header_height)endfunction draw_title(pane)assert(pane.title)if Text_cache[pane.title] == nil thenText_cache[pane.title] = App.newText(love.graphics.getFont(), pane.title)endApp.color(Pane_title_color)App.screen.draw(Text_cache[pane.title], pane.left, pane.top-Margin_above -5-Line_height)App.color(Pane_title_background_color)love.graphics.rectangle('fill', pane.left-Margin_left, pane.top-Margin_above-5-Line_height-5, Margin_left+Display_settings.column_width+Margin_right, 5+Line_height+5)function draw_links(pane)local links = Cache[pane.id].linksif links == nil then return endif empty(links) then return endlocal x = pane.leftfor _,label in ipairs(Edge_list) doif Text_cache[label] == nil thenText_cache[label] = App.newText(love.graphics.getFont(), label)endif links[label] thendraw_link(label, x, pane.bottom)endx = x + App.width(Text_cache[label]) + 10 + 10end-- links we don't know about, just in casefor link,_ in pairs(links) doif not Opposite[link] thenif Text_cache[link] == nil thenText_cache[link] = App.newText(love.graphics.getFont(), link)enddraw_link(link, x, pane.bottom)x = x + App.width(Text_cache[link]) + 10 + 10endendpane.bottom = pane.bottom + 5+Line_height+5endfunction draw_link(label, x,y)App.color(Crosslink_color)love.graphics.draw(Text_cache[label], x, y+5)App.color(Crosslink_background_color)love.graphics.rectangle('fill', x-5, y+3, App.width(Text_cache[label])+10, 2+Line_height+2)end-- assumes intervals are half-open: [lo, hi)-- https://en.wikipedia.org/wiki/Interval_(mathematics)function overlap(lo1,hi1, lo2,hi2)-- lo2 hi2-- | |-- | |-- | |if lo1 <= lo2 and hi1 > lo2 thenreturn trueend-- lo2 hi2-- | |-- | |if lo1 < hi2 and hi1 >= hi2 thenreturn trueend-- lo2 hi2-- | |-- | |return lo1 >= lo2 and hi1 <= hi2function App.update(dt)Cursor_time = Cursor_time + dt-- some hysteresis while resizingif App.getTime() < Last_resize_time + 0.1 thenreturnendif App.mouse_y() < Header_height then-- column headerlove.mouse.setCursor(love.mouse.getSystemCursor('arrow'))elseif in_pane(App.mouse_x(), App.mouse_y()) thenlove.mouse.setCursor(love.mouse.getSystemCursor('arrow'))elselove.mouse.setCursor(love.mouse.getSystemCursor('hand'))endif Pan.x thenDisplay_settings.x = math.max(Pan.x-App.mouse_x(), 0)Display_settings.y = math.max(Pan.y-(App.mouse_y()-Header_height), 0)endif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane and pane.editable thenedit.update(pane, dt)endendif not Display_settings.show_palette and (Display_settings.mode == 'normal' or Display_settings.mode == 'search') and App.mouse_down(1) then-- pan the surface by draggingplan_draw()endif Display_settings.mode == 'searching_all' thenresume_search_all()endendfunction in_pane(x,y)-- duplicate some logic from App.drawlocal sx,sy = to_surface(x,y)local x = Padding_horizontalfor column_idx, column in ipairs(Surface) doif sx < x thenreturn falseendif sx < x + Margin_left + Display_settings.column_width + Margin_right thenlocal y = Padding_verticalfor pane_idx, pane in ipairs(column) doif sy < y thenreturn falseendif sy < y + Margin_above + height(pane) + Margin_below thenreturn trueendy = y + Margin_above + height(pane) + Margin_below + Padding_verticalendendx = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontalendreturn falseendfunction to_pane(sx,sy)-- duplicate some logic from App.drawlocal x = Padding_horizontalfor column_idx, column in ipairs(Surface) doif sx < x thenreturn nilendif sx < x + Margin_left + Display_settings.column_width + Margin_right thenlocal y = Padding_verticalfor pane_idx, pane in ipairs(column) doif sy < y thenreturn nilendif sy < y + Margin_above + height(pane) + Margin_below thenreturn {col=column_idx, row=pane_idx}endy = y + Margin_above + height(pane) + Margin_below + Padding_verticalendendx = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontalendreturn nilendfunction to_surface(x, y)return x+Display_settings.x, y+Display_settings.y-Header_heightendfunction love.quit()if Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane and pane.editable thenedit.quit(pane)endend-- save some important settingslocal x,y,displayindex = love.window.getPosition()local column_names = {}for _,column in ipairs(Surface) dotable.insert(column_names, column.name)endlocal settings = {x=x, y=y, displayindex=displayindex,width=App.screen.width, height=App.screen.height,font_height=Font_height,column_width=Display_settings.column_width,surface_x=Display_settings.x,surface_y=Display_settings.y,cursor_col=Cursor_pane.col,cursor_row=Cursor_pane.row,columns=column_names,}love.filesystem.write(Settings_file, json.encode(settings))endfunction App.mousepressed(x,y, mouse_button)--? print('app mouse pressed', x,y)Cursor_time = 0 -- ensure cursor is visible immediately after it movesclear_selections()if Display_settings.mode == 'normal' or Display_settings.mode == 'search' or Display_settings.mode == 'search_all' or Display_settings.mode == 'searching_all' thenmouse_pressed_in_normal_mode(x,y, mouse_button)elseif Display_settings.mode == 'maximize' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenedit.mouse_pressed(pane, x,y, mouse_button)endendelseprint(Display_settings.mode)assert(false)endendfunction clear_selections()for _,column in ipairs(Surface) dofor _,pane in ipairs(column) dopane.selection1 = {}endendendfunction mouse_pressed_in_normal_mode(x,y, mouse_button)Pan = {}if y < Header_height then-- column headers currently not interactablereturnendlocal sx,sy = to_surface(x,y)if in_pane(x,y) then--? print('click on pane')Cursor_pane = to_pane(sx,sy)if Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenedit.mouse_pressed(pane, x,y, mouse_button)pane._height = nilendend
if Current_app == 'run' thenif run.keychord_pressed then run.keychord_pressed(chord, key) endelseif Current_app == 'source' thenif source.keychord_pressed then source.keychord_pressed(chord, key) end
Pan = {x=sx, y=sy}endendfunction App.mousereleased(x,y, mouse_button)--? print('app mouse released')Cursor_time = 0 -- ensure cursor is visible immediately after it movesif Cursor_pane.col >= 1 thenedit.mouse_released(Surface[Cursor_pane.col][Cursor_pane.row], x,y, mouse_button)endPan = {}endfunction App.focus(in_focus)if in_focus thenLast_focus_time = App.getTime()
assert(false, 'unknown app "'..Current_app..'"')
Cursor_time = 0 -- ensure cursor is visible immediately after it moves--? print('textinput', t)-- hotkeys operating on the cursor paneif Display_settings.show_palette thenDisplay_settings.palette_command = Display_settings.palette_command..tDisplay_settings.palette_command_text = App.newText(love.graphics.getFont(), Display_settings.palette_command)Display_settings.palette_alternative_index = 1Display_settings.palette_candidates = candidates()elseif Display_settings.mode == 'normal' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenif not pane.editable then-- global hotkeys for normal modeif t == 'X' thencommand.wider_columns()returnelseif t == 'x' thencommand.narrower_columns()returnend-- send keys to the current paneelseif pane.cursor_x >= 0 and pane.cursor_x < App.screen.width thenif pane.cursor_y >= Header_height and pane.cursor_y < App.screen.height then--? print(('%s typed in editor pane'):format(t))local old_top = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}edit.textinput(pane, t)maybe_update_screen_top_of_cursor_pane(pane, old_top)pane._height = nilplan_draw()endendendendendelseif Display_settings.mode == 'search' then--? print('insert', t)Display_settings.search_term = Display_settings.search_term..tDisplay_settings.search_text = nil-- reset search stateclear_selections()Display_settings.x = Display_settings.search_backup_xDisplay_settings.y = Display_settings.search_backup_yCursor_pane = Display_settings.search_backup_cursor_pane-- search againsearch_next()bring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = trueplan_draw()elseif Display_settings.mode == 'search_all' thenDisplay_settings.search_all_query = Display_settings.search_all_query..tDisplay_settings.search_all_query_text = nilelseif Display_settings.mode == 'searching_all' thenDisplay_settings.mode = 'normal'Display_settings.search_all_query_text = nilelseif Display_settings.mode == 'maximize' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenif pane.editable thenedit.textinput(pane, t)endendendelseprint(Display_settings.mode)assert(false)endendfunction App.keychord_pressed(chord, key)-- ignore events for some time after window in focusif App.getTime() < Last_focus_time + 0.01 thenreturnend--? print('keychord press', chord)Cursor_time = 0 -- ensure cursor is visible immediately after it moves-- global hotkeysif chord == 'C-=' thenupdate_font_settings(Font_height+2)elseif chord == 'C--' thenupdate_font_settings(Font_height-2)elseif chord == 'C-0' thenupdate_font_settings(20)-- mode-specific hotkeyselseif Display_settings.show_palette thenkeychord_pressed_on_command_palette(chord, key)elseif Display_settings.mode == 'normal' thenif chord == 'C-return' thenDisplay_settings.show_palette = trueDisplay_settings.palette_candidates = candidates()elseif chord == 'C-f' thencommand.commence_find_on_surface()elseif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane and pane.editable thenkeychord_pressed_on_editable_pane(pane, chord, key)elsekeychord_pressed_in_normal_mode_with_immutable_pane(pane, chord, key)end-- editable cursor pane will have already updated its screen_top, so don't clobber it hereplan_draw{ignore_editable_cursor_pane=true}endelseif Display_settings.mode == 'search' thenkeychord_pressed_in_search_mode(chord, key)elseif Display_settings.mode == 'search_all' thenkeychord_pressed_in_search_all_mode(chord, key)elseif Display_settings.mode == 'searching_all' theninterrupt_search_all()elseif Display_settings.mode == 'maximize' thenif chord == 'C-return' thenDisplay_settings.show_palette = trueDisplay_settings.palette_candidates = candidates()elsekeychord_pressed_in_maximize_mode(chord, key)endelseprint(Display_settings.mode)assert(false)endendfunction update_font_settings(font_height)local column_width_in_ems = Display_settings.column_width / App.width(Em)Font_height = font_heightlove.graphics.setFont(love.graphics.newFont(Font_height))Line_height = math.floor(font_height*1.3)Em = App.newText(love.graphics.getFont(), 'm')Display_settings.column_width = column_width_in_ems*App.width(Em)for _,column in ipairs(Surface) dofor _,pane in ipairs(column) dopane.font_height = Font_heightpane.line_height = Line_heightpane.em = Empane.left = 0pane.right = Display_settings.column_widthendendclear_all_pane_heights()plan_draw()end-- Scan all panes, while delegating as much work as possible to lines.love search.-- * Text.search_next in lines.love scans from cursor while wrapping around-- within the pane, so we need to work around that.-- * Each pane's search_term field influences whether the search term at-- cursor is highlighted, so we need to manage that as well. At any moment-- we want the search_term and search_text to be set for at most a single-- pane.---- Side-effect: we perturb the cursor of panes as we scan them.function search_next()if Cursor_pane.col < 1 then return endclear_all_search_terms()local pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane == nil thenreturnend--? print('search next', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)local old_cursor_in_cursor_pane = {line=pane.cursor1.line, pos=pane.cursor1.pos}-- scan current pane down from cursorif search_next_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) then--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)returnendpane.cursor1 = old_cursor_in_cursor_pane-- scan current column down from current panefor current_pane_index=Cursor_pane.row+1,#Surface[Cursor_pane.col] dolocal pane = Surface[Cursor_pane.col][current_pane_index]pane.cursor1 = {line=1, pos=1}edit.fixup_cursor(pane)pane.screen_top1 = {line=1, pos=1}if search_next_in_pane(pane) thenCursor_pane.row = current_pane_index--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendendlocal current_column_index = 1 + Cursor_pane.col%#Surface -- (i+1)%#Surface in the presence of 1-indexing-- scan columns past current, looping aroundwhile true dofor current_pane_index,pane in ipairs(Surface[current_column_index]) dopane.cursor1 = {line=1, pos=1}edit.fixup_cursor(pane)pane.screen_top1 = {line=1, pos=1}if search_next_in_pane(pane) thenCursor_pane = {col=current_column_index, row=current_pane_index}--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendend-- loop updatecurrent_column_index = 1 + current_column_index%#Surface -- i = (i+1)%#Surface in the presence of 1-indexing-- termination checkif current_column_index == Cursor_pane.col thenbreakendend-- scan current column until current panefor current_pane_index=1,Cursor_pane.row-1 doif search_next_in_pane(Surface[Cursor_pane.col][current_pane_index]) thenCursor_pane.row = current_pane_index--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendend-- finally, scan the cursor pane until the cursorlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]local old_cursor = pane.cursor1pane.cursor1 = {line=1, pos=1}edit.fixup_cursor(pane)pane.screen_top1 = {line=1, pos=1}if search_next_in_pane(pane) thenif Text.lt1(pane.cursor1, old_cursor) thenreturnendend-- nothing foundpane.cursor1 = old_cursor_in_cursor_paneend-- returns whether it found an occurrencefunction search_next_in_pane(pane)pane.search_term = Display_settings.search_termpane.search_text = Display_settings.search_textpane.search_backup = {cursor={line=pane.cursor1.line, pos=pane.cursor1.pos}, screen_top={line=pane.screen_top1.line, pos=pane.screen_top1.pos}}for i=1,#pane.lines doif pane.line_cache[i] == nil thenpane.line_cache[i] = {}endendif Text.search_next(pane) thenif Text.le1(pane.search_backup.cursor, pane.cursor1) then-- select this occurrencereturn trueend-- Otherwise cursor wrapped around. Skip this pane.end-- Clean up this pane before moving on to the next one.pane.search_term = nilpane.search_text = nilpane.cursor1.line = pane.search_backup.cursor.linepane.cursor1.pos = pane.search_backup.cursor.pospane.screen_top1.line = pane.search_backup.screen_top.linepane.screen_top1.pos = pane.search_backup.screen_top.pospane.search_backup = nilend-- Scan all panes, while delegating as much work as possible to lines.love search.function search_previous()if Cursor_pane.col < 1 then return endclear_all_search_terms()local pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane == nil thenreturnend--? print('search previous', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)local old_cursor_in_cursor_pane = {line=pane.cursor1.line, pos=pane.cursor1.pos}-- scan current pane up from cursorif search_previous_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) then--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)returnendpane.cursor1 = old_cursor_in_cursor_pane-- scan current column down from current panefor current_pane_index=Cursor_pane.row-1,1,-1 dolocal pane = Surface[Cursor_pane.col][current_pane_index]pane.cursor1 = edit.final_cursor(pane)if search_previous_in_pane(pane) thenCursor_pane.row = current_pane_index--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendendlocal current_column_index = 1 + (Cursor_pane.col-2)%#Surface -- (i-1)%#Surface in the presence of 1-indexing-- scan columns past current, looping aroundwhile true dofor current_pane_index = #Surface[current_column_index],1,-1 dolocal pane = Surface[current_column_index][current_pane_index]pane.cursor1 = edit.final_cursor(pane)if search_previous_in_pane(pane) thenCursor_pane = {col=current_column_index, row=current_pane_index}--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendend-- loop updatecurrent_column_index = 1 + (current_column_index-2)%#Surface -- i = (i-1)%#Surface in the presence of 1-indexing-- termination checkif current_column_index == Cursor_pane.col thenbreakendend-- scan current column from bottom current panefor current_pane_index=#Surface[Cursor_pane.col],Cursor_pane.row+1,-1 do--? print('same column', current_pane_index)if search_previous_in_pane(Surface[Cursor_pane.col][current_pane_index]) thenCursor_pane.row = current_pane_index--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)returnendend-- finally, scan the cursor pane from bottom until cursorlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]local old_cursor = pane.cursor1pane.cursor1 = edit.final_cursor(pane)if search_previous_in_pane(pane) thenif Text.lt1(old_cursor, pane.cursor1) thenreturnendend-- nothing foundpane.cursor1 = old_cursor_in_cursor_paneend-- returns whether it found an occurrencefunction search_previous_in_pane(pane)pane.search_term = Display_settings.search_termpane.search_text = Display_settings.search_textpane.search_backup = {cursor={line=pane.cursor1.line, pos=pane.cursor1.pos}, screen_top={line=pane.screen_top1.line, pos=pane.screen_top1.pos}}for i=1,#pane.lines doif pane.line_cache[i] == nil thenpane.line_cache[i] = {}endendif Text.search_previous(pane) thenif Text.lt1(pane.cursor1, pane.search_backup.cursor) then-- select this occurrencereturn trueend-- Otherwise cursor wrapped around. Skip this pane.end-- Clean up this pane before moving on to the previous one.pane.search_term = nilpane.search_text = nilpane.cursor1.line = pane.search_backup.cursor.linepane.cursor1.pos = pane.search_backup.cursor.pospane.screen_top1.line = pane.search_backup.screen_top.linepane.screen_top1.pos = pane.search_backup.screen_top.pospane.search_backup = nilendfunction bring_cursor_of_cursor_pane_in_view(dir)if Cursor_pane.col < 1 thenreturnendlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane == nil thenreturnend--? print('viewport before', Display_settings.x, Display_settings.y)local left_edge_sx = left_edge_sx(Cursor_pane.col)local cursor_sx = left_edge_sx + Text.x_of_schema1(pane, pane.cursor1)local vertically_ok = cursor_sx > Display_settings.x and cursor_sx < Display_settings.x + App.screen.width - App.width(Em)--? print(y_of_schema1(pane, pane.cursor1))--? print('viewport starts at', Display_settings.y)--? print('pane starts at', up_edge_sy(Cursor_pane.col, Cursor_pane.row))--? print('cursor line contains ^'..pane.lines[pane.cursor1.line].data..'$')--? print('cursor is at', y_of_schema1(pane, pane.cursor1), 'from top of pane')local cursor_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.cursor1)--? print('cursor is at', cursor_sy)local horizontally_ok = cursor_sy > Display_settings.y and cursor_sy < Display_settings.y + App.screen.height - Header_height - 2*Line_height -- account for search bar along the bottomif vertically_ok and horizontally_ok thenreturnendif dir == 'up' thenif not vertically_ok thenDisplay_settings.x = left_edge_sx - Margin_left - Padding_horizontalendif not horizontally_ok thenDisplay_settings.y = cursor_sy - 3*Line_heightendelseassert(dir == 'down')if not vertically_ok thenDisplay_settings.x = left_edge_sx + Display_settings.column_width + Margin_right + Padding_horizontal - App.screen.widthendif not horizontally_ok then--? print('cursor used to be at ', cursor_sy - Display_settings.y)--? print('subtract', App.screen.height, App.screen.height-Header_height)Display_settings.y = cursor_sy + Text.search_bar_height(pane) - (App.screen.height - Header_height)-- Bah, temporarily giving up on debugging.Display_settings.y = Display_settings.y + Line_height--? print('=>', Display_settings.y)--? print('cursor now at ', cursor_sy - Display_settings.y)--? print('viewport height', App.screen.height)--? print('cursor row starts', App.screen.height - (cursor_sy-Display_settings.y), 'px above bottom of viewport') -- totally wrongassert(App.screen.height - (cursor_sy-Display_settings.y) > 1.5*Line_height)endend--? print('viewport before clamp', Display_settings.x, Display_settings.y)Display_settings.x = math.max(Display_settings.x, 0)Display_settings.y = math.max(Display_settings.y, 0)--? print('viewport now', Display_settings.x, Display_settings.y)endfunction clear_all_search_terms()for col,column in ipairs(Surface) dofor row,pane in ipairs(column) dopane.search_term = nilpane.search_text = nilendendendfunction keychord_pressed_in_maximize_mode(chord, key)if Cursor_pane.col < 1 thenprint('no current note to edit')returnendlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane == nil thenprint('no current note to edit')returnendif pane.editable thenif chord == 'C-e' thencommand.exit_editing()elseedit.keychord_pressed(pane, chord, key)endelseif chord == 'C-e' thencommand.edit_note()elseif chord == 'C-c' thenedit.keychord_pressed(pane, chord, key)endendendfunction keychord_pressed_on_editable_pane(pane, chord, key)-- ignore if cursor is not visible on screenif pane.cursor_x == nil thenassert(pane.cursor_y == nil)panning_keychord_pressed(chord, key)returnendif chord == 'C-e' thencommand.exit_editing()else--? print(('%s pressed in editor pane'):format(chord))--? print(pane.cursor_x, pane.cursor_y)local old_top = {line=pane.screen_top1.line, pos=pane.screen_top1.pos}edit.keychord_pressed(pane, chord, key)maybe_update_screen_top_of_cursor_pane(pane, old_top)pane._height = nilendendfunction maybe_update_screen_top_of_cursor_pane(pane, old_top)local cursor_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.cursor1)--? print(eq(old_top, pane.screen_top1), eq(old_top, {line=1, pos=1}), pane.top, cursor_sy, cursor_sy - Display_settings.y, App.screen.height - Header_height - Line_height)if not eq(old_top, pane.screen_top1) and eq(old_top, {line=1, pos=1}) and pane.top > Header_height and cursor_sy - Display_settings.y > App.screen.height - Header_height - Line_height then-- pan the surface instead of scrolling within the panepane.screen_top1 = old_topbring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = true -- cursor was on screen before keystroke, so it should remain on screen afterreturnendEditable_cursor_pane_updated_screen_top = not eq(old_top, pane.screen_top1)if Editable_cursor_pane_updated_screen_top then--? print(('screen top changed from (%d,%d) to (%d,%d)'):format(old_top.line, old_top.pos, pane.screen_top1.line, pane.screen_top1.pos))--? print('updating viewport based on screen top')--? print('from', Display_settings.y, y_of_schema1(pane, pane.screen_top1))Display_settings.y = up_edge_sy(Cursor_pane.col, Cursor_pane.row) + y_of_schema1(pane, pane.screen_top1)--? print('to', Display_settings.y)Surface.cursor_on_screen_check = true -- cursor was on screen before keystroke, so it should remain on screen afterendendfunction keychord_pressed_in_normal_mode_with_immutable_pane(pane, chord, key)-- return if no part of cursor pane is visiblelocal left_sx = left_edge_sx(Cursor_pane.col)if not should_show_column(left_sx) thenpanning_keychord_pressed(chord, key)returnendlocal up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)if not should_show_pane(pane, up_sy) thenpanning_keychord_pressed(chord, key)returnendif chord == 'C-e' thencommand.edit_note()elseif chord == 'C-c' thenedit.keychord_pressed(pane, chord, key)elsepanning_keychord_pressed(chord, key)endend-- y offset of a given (line, pos)function y_of_schema1(pane, loc)--? print(('updating viewport y; cursor pane starts at %d; screen top is at %d,%d'):format(result, loc.line, loc.pos))local result = 0if pane.title thenresult = result + 5+Line_height+5endresult = result + Margin_aboveif loc.line == 1 and loc.pos == 1 thenreturn resultendfor i=1,loc.line-1 do--? print('', 'd', i, result)Text.populate_screen_line_starting_pos(pane, i)--? print('', '', #pane.line_cache[i].screen_line_starting_pos, pane.left, pane.right)result = result + line_height(pane, i, pane.left, pane.right)endif pane.lines[loc.line].mode == 'text' thenText.populate_screen_line_starting_pos(pane, loc.line)for i,screen_line_starting_pos in ipairs(pane.line_cache[loc.line].screen_line_starting_pos) doif screen_line_starting_pos >= loc.pos thenbreakendresult = result + Line_heightendend--? print(('viewport at %d'):format(result))return resultendfunction keychord_pressed_in_search_mode(chord, key)if chord == 'escape' thenDisplay_settings.mode = 'normal'clear_all_search_terms()clean_up_panes()-- go back to old viewport--? print('esc; exiting search mode')Display_settings.x = Display_settings.search_backup_xDisplay_settings.y = Display_settings.search_backup_yCursor_pane = Display_settings.search_backup_cursor_pane-- don't forget search textelseif chord == 'return' thenDisplay_settings.mode = 'normal'clear_all_search_terms()clean_up_panes()-- forget old viewport--? print('return; exiting search mode')Display_settings.search_backup_x = nilDisplay_settings.search_backup_y = nilDisplay_settings.search_backup_cursor_pane = nil-- don't forget search textelseif chord == 'backspace' thenlocal len = utf8.len(Display_settings.search_term)local byte_offset = Text.offset(Display_settings.search_term, len)Display_settings.search_term = string.sub(Display_settings.search_term, 1, byte_offset-1)Display_settings.search_text = nil-- reset search stateclear_selections()Display_settings.x = Display_settings.search_backup_xDisplay_settings.y = Display_settings.search_backup_yCursor_pane = Display_settings.search_backup_cursor_pane-- search againsearch_next()bring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = trueplan_draw()--? print('backspace; search term is now', Display_settings.search_term)elseif chord == 'C-v' thenDisplay_settings.search_term = Display_settings.search_term..App.getClipboardText()Display_settings.search_text = nil-- reset search stateclear_selections()Display_settings.x = Display_settings.search_backup_xDisplay_settings.y = Display_settings.search_backup_yCursor_pane = Display_settings.search_backup_cursor_pane-- search againsearch_next()bring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = trueplan_draw()--? print('paste; search term is now', Display_settings.search_term)elseif chord == 'up' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thensearch_previous()bring_cursor_of_cursor_pane_in_view('up')Surface.cursor_on_screen_check = trueplan_draw()endendelseif chord == 'down' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenpane.cursor1.pos = pane.cursor1.pos+1search_next()bring_cursor_of_cursor_pane_in_view('down')Surface.cursor_on_screen_check = trueplan_draw()endend-- things from normal mode we still wantelseif chord == 'C-c' thenif Cursor_pane.col >= 1 thenlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane thenedit.keychord_pressed(pane, chord, key)endendendendfunction keychord_pressed_in_search_all_mode(chord, key)if chord == 'escape' thenDisplay_settings.mode = 'normal'-- don't forget search textDisplay_settings.search_all_state = nilelseif chord == 'return' thenfinalize_search_all_pane()add_search_all_pane_to_right_of_cursor()Display_settings.mode = 'searching_all'plan_draw()elseif chord == 'backspace' thenlocal len = utf8.len(Display_settings.search_all_query)local byte_offset = Text.offset(Display_settings.search_all_query, len)Display_settings.search_all_query = string.sub(Display_settings.search_all_query, 1, byte_offset-1)Display_settings.search_all_query_text = nil--? print('backspace; search_all term is now', Display_settings.search_all_query)elseif chord == 'C-v' thenDisplay_settings.search_all_query = Display_settings.search_all_query..App.getClipboardText()Display_settings.search_all_query_text = nil--? print('paste; search_all term is now', Display_settings.search_all_query)endend-- return (line, pos) of the screen line starting near a given y offset, and-- y_offset remaining after the calculation-- invariants:-- - 0 <= y_offset <= Line_height if line is text-- - let loc, y_offset = schema1_of_y(pane, y)-- y - y_offset == y_of_schema1(pane, loc)function schema1_of_y(pane, y)assert(y >= 0)local y_offset = yfor i=1,#pane.lines do--? print('--', y_offset)Text.populate_screen_line_starting_pos(pane, i)local height = line_height(pane, i, pane.left, pane.right)if y_offset < height thenlocal line = pane.lines[i]if line.mode ~= 'text' thenreturn {line=i, pos=1}, y_offsetelselocal nlines = math.floor(y_offset/pane.line_height)--? print(y_offset, pane.line_height, nlines)assert(nlines >= 0 and nlines < #pane.line_cache[i].screen_line_starting_pos)local pos = pane.line_cache[i].screen_line_starting_pos[nlines+1] -- switch to 1-indexingy_offset = y_offset - nlines*pane.line_heightreturn {line=i, pos=pos}, y_offsetendendy_offset = y_offset - heightend-- y is below the panereturn {line=#pane.lines+1, pos=1}, y_offsetendfunction line_height(State, line_index, left, right)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]if line.mode == 'text' thenreturn Line_height*#line_cache.screen_line_starting_poselsereturn Drawing.pixels(line.h, right-left) + Drawing_padding_heightendendfunction stop_editing_all()local edit_count = 0for _,column in ipairs(Surface) dofor _,pane in ipairs(column) doif pane.editable thenstop_editing(pane)edit_count = edit_count+1endendendassert(edit_count <= 1)endfunction stop_editing(pane)edit.quit(pane)-- save symmetric linksfor rel,target in pairs(Cache[pane.id].links) doinitialize_cache_if_necessary(target)save_links(target)endif Display_settings.mode ~= 'maximize' thenrefresh_panes(pane)endpane.editable = falseendfunction panning_keychord_pressed(chord, key)if chord == 'up' thenDisplay_settings.y = math.max(Display_settings.y - Pan_step, 0)local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)local up_py = up_sy - Display_settings.yif up_py > 2/3*App.screen.height thenCursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))endelseif chord == 'down' thenlocal visible_column_max_y = most(column_height, visible_columns())if visible_column_max_y - Display_settings.y > App.screen.height/2 thenDisplay_settings.y = Display_settings.y + Pan_stependlocal down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)local down_px = down_sx - Display_settings.yif down_px < App.screen.height/3 thenCursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))endelseif chord == 'left' thenDisplay_settings.x = math.max(Display_settings.x - Pan_step, 0)local left_sx = left_edge_sx(Cursor_pane.col)local left_px = left_sx - Display_settings.xif left_px > App.screen.width - Margin_right - Display_settings.column_width/2 thenCursor_pane.col = math.min(#Surface, col(Display_settings.x + App.screen.width - Margin_right - Display_settings.column_width/2))Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)endelseif chord == 'right' thenif Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) thenDisplay_settings.x = Display_settings.x + Pan_stependlocal right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_widthlocal right_px = right_sx - Display_settings.xif right_px < Margin_left + Display_settings.column_width/2 thenCursor_pane.col = math.min(#Surface, col(Display_settings.x + Margin_left + Display_settings.column_width/2))Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)endelseif chord == 'pageup' or chord == 'S-up' thenDisplay_settings.y = math.max(Display_settings.y - App.screen.height + Line_height*2, 0)local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)local up_py = up_sy - Display_settings.yif up_py > 2/3*App.screen.height thenCursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))endelseif chord == 'pagedown' or chord == 'S-down' then--? print('pagedown')local visible_column_max_y = most(column_height, visible_columns())if visible_column_max_y - Display_settings.y > App.screen.height then--? print('updating viewport')Display_settings.y = Display_settings.y + App.screen.height - Line_height*2endlocal down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)local down_px = down_sx - Display_settings.yif down_px < App.screen.height/3 then--? print('updating row')Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))--? print('=>', Cursor_pane.row)endelseif chord == 'S-left' thenDisplay_settings.x = math.max(Display_settings.x - Margin_left - Display_settings.column_width - Margin_right - Padding_horizontal, 0)local left_sx = left_edge_sx(Cursor_pane.col)local left_px = left_sx - Display_settings.xif left_px > App.screen.width - Margin_right - Display_settings.column_width/2 thenCursor_pane.col = math.min(#Surface, col(Display_settings.x + App.screen.width - Margin_right - Display_settings.column_width/2))Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)endelseif chord == 'S-right' thenif Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) thenDisplay_settings.x = Display_settings.x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontallocal right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_widthlocal right_px = right_sx - Display_settings.xif right_px < Margin_left + Display_settings.column_width/2 thenCursor_pane.col = math.min(#Surface, col(Display_settings.x + Margin_left + Display_settings.column_width/2))Cursor_pane.row = math.min(#Surface[Cursor_pane.col], Cursor_pane.row)endendelseif chord == 'C-down' thencommand.down_one_pane()elseif chord == 'C-up' thencommand.up_one_pane()elseif chord == 'C-end' thencommand.bottom_pane_of_column()elseif chord == 'C-home' thencommand.top_pane_of_column()end--? print('after', Cursor_pane.col, Cursor_pane.row)endfunction visible_columns()local result = {}local col = col(Display_settings.x)local x = left_edge_sx(col) - Display_settings.xwhile col <= #Surface dox = x + Padding_horizontaltable.insert(result, col)x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontalif x > App.screen.width thenbreakendcol = col+1endreturn resultendfunction refresh_panes(pane)--? print('refreshing')Cache[pane.id].lines = pane.linesfor x,col in ipairs(Surface) dofor y,p in ipairs(col) doif p.id == pane.id then--? print(x,y)p.lines = pane.linesp._height = nilText.redraw_all(p)endendendplan_draw()endfunction clean_up_panes()for x,col in ipairs(Surface) dofor y,p in ipairs(col) dop._height = nilText.redraw_all(p)endendplan_draw()endfunction App.keyreleased(key, scancode)-- ignore events for some time after window in focusif App.getTime() < Last_focus_time + 0.01 thenreturnend--? print('key release', key)Cursor_time = 0 -- ensure cursor is visible immediately after it movesif Cursor_pane.col < 1 thenreturnendlocal pane = Surface[Cursor_pane.col][Cursor_pane.row]if pane and pane.editable thenedit.key_released(pane, key, scancode)endendfunction clear_all_pane_heights()Text_cache = {}for _,column in ipairs(Surface) dofor _,pane in ipairs(column) dopane._height = nilendendend-- convert x surface pixel coordinate into column indexfunction col(x)return 1 + math.floor(x / (Padding_horizontal + Display_settings.column_width))end-- col is 1-indexed-- returns x surface pixel coordinate of left edge of column colfunction left_edge_sx(col)return (col-1)*(Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) + Padding_horizontal + Margin_leftendfunction row(col, y)local sy = Padding_verticalfor i,pane in ipairs(Surface[col]) do--? print('', i, y, sy, next_sy)local next_sy = sy + Margin_above + height(pane) + Margin_below + Padding_verticalif next_sy > y thenreturn iendsy = next_syendreturn #Surface[col]endfunction up_edge_sy(col, row)local result = Padding_verticalfor i=1,row-1 dolocal pane = Surface[col][i]result = result + Margin_above + height(pane) + Margin_below + Padding_verticalendreturn resultendfunction down_edge_sx(col, row)local result = Padding_verticalfor i=1,row dolocal pane = Surface[col][i]result = result + Margin_above + height(pane) + Margin_below + Padding_verticalendreturn result - Padding_verticalendfunction column_height(col)local result = Padding_verticalfor pane_index, pane in ipairs(Surface[col]) doresult = result + Margin_above + height(pane) + Margin_below + Padding_verticalendreturn resultendfunction most(f, arr)local result = nilfor _,x in ipairs(arr) dolocal curr = f(x)if result == nil or result < curr thenresult = currendendreturn resultendif Current_app == 'run' thenif run.keychord_pressed then run.keychord_pressed(chord, key) endelseif Current_app == 'source' thenif source.keychord_pressed then source.keychord_pressed(chord, key) endelseassert(false, 'unknown app "'..Current_app..'"')-- ignore events for some time after window in focus (mostly alt-tab)
--if Current_app == 'run' thenif run.textinput then run.textinput(t) endelseif Current_app == 'source' thenif source.textinput then source.textinput(t) endelseassert(false, 'unknown app "'..Current_app..'"')endfunction App.keyreleased(chord, key)-- ignore events for some time after window in focus (mostly alt-tab)--if Current_app == 'run' thenif run.key_released then run.key_released(chord, key) endelseif Current_app == 'source' thenif source.key_released then source.key_released(chord, key) endelseassert(false, 'unknown app "'..Current_app..'"')endendfunction App.mousepressed(x,y, mouse_button)--? print('mouse press', x,y)if Current_app == 'run' thenif run.mouse_pressed then run.mouse_pressed(x,y, mouse_button) endelseif Current_app == 'source' thenif source.mouse_pressed then source.mouse_pressed(x,y, mouse_button) endelseassert(false, 'unknown app "'..Current_app..'"')endfunction App.mousereleased(x,y, mouse_button)if Current_app == 'run' thenif run.mouse_released then run.mouse_released(x,y, mouse_button) endelseif Current_app == 'source' thenif source.mouse_released then source.mouse_released(x,y, mouse_button) endelseassert(false, 'unknown app "'..Current_app..'"')
function num_panes()local result = 0for _,column in ipairs(Surface) doresult = result+#columnendreturn resultendCursor_pane.col = math.min(Cursor_pane.col, #Surface)if Cursor_pane.col >= 1 thenCursor_pane.row = math.min(Cursor_pane.row, #Surface[Cursor_pane.col])endplan_draw()endendfunction load_settings()local settings = json.decode(love.filesystem.read(Settings_file))-- maximize window to determine maximum allowable dimensionslove.window.setMode(0, 0) -- maximizeApp.screen.width, App.screen.height, App.screen.flags = love.window.getMode()--? print('max height', App.screen.height)-- set up desired window dimensionsApp.screen.flags.resizable = trueApp.screen.flags.minwidth = math.min(App.screen.width, 200)App.screen.flags.minheight = math.min(App.screen.width, 200)App.screen.width, App.screen.height = settings.width, settings.heightlove.window.setMode(App.screen.width, App.screen.height, App.screen.flags)love.window.setPosition(settings.x, settings.y, settings.displayindex)Font_height = settings.font_heightLine_height = math.floor(Font_height*1.3)love.graphics.setFont(love.graphics.newFont(Font_height))Em = App.newText(love.graphics.getFont(), 'm')Display_settings.column_width = settings.column_widthfor _,column_name in ipairs(settings.columns) docreate_column(column_name)endCursor_pane.col = settings.cursor_colCursor_pane.row = settings.cursor_rowDisplay_settings.x = settings.surface_xDisplay_settings.y = settings.surface_yendfunction initialize_default_settings()initialize_window_geometry()love.graphics.setFont(love.graphics.newFont(Font_height))Em = App.newText(love.graphics.getFont(), 'm')Display_settings.column_width = 40*App.width(Em)-- initialize surface with a single columncommand.recently_modified()end-- for hysteresis in a few placesLast_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700Last_resize_time = App.getTime()function App.initialize(arg)if Current_app == 'run' thenrun.initialize(arg)elseif Current_app == 'source' thensource.initialize(arg)elseassert(false, 'unknown app "'..Current_app..'"')endfunction initialize_window_geometry()App.screen.width = App.screen.width-100
* `ctrl+e` to modify the sources
## Modifying the appHit `ctrl+u` from within the to modify its code. The infrastructure works, butit isn't advertized within the app because this particular app is currentlytoo large to comfortably modify from within itself. I use more specializededitors while I improve the editing infrastructure further.
- delete app settings, start; window opens running the text editor- quit while running the text editor, restart; window opens running the text editor in same position+dimensions
- delete app settings, start; window opens running the note-taking app- quit while running the note-taking app, restart; window opens running the note-taking app in same position+dimensions
- start out running the text editor, move window, press ctrl+e twice; window is running text editor in same position+dimensions
- start out running the note-taking app, move window, press ctrl+e twice; window is running note-taking app in same position+dimensions
* run love with directory; text editor runs* run love with zip file; text editor runs
* run love with directory; note-taking app runs* run love with zip file; note-taking app runs* start out in the note-taking app, press ctrl+e to edit source, make a change to the source, press ctrl+e twice to return to the source editor; the change should be preserved.