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 tests
App.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 = 5
if Show_file_navigator then
source.draw_file_navigator()
return
end
add_hotkey_to_menu('ctrl+e: run')
if Focus == 'edit' then
add_hotkey_to_menu('ctrl+g: switch file')
if Show_log_browser_side then
add_hotkey_to_menu('ctrl+l: hide log browser')
else
add_hotkey_to_menu('ctrl+l: show log browser')
end
if Editor_state.expanded then
add_hotkey_to_menu('ctrl+b: collapse debug prints')
else
add_hotkey_to_menu('ctrl+b: expand debug prints')
end
add_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 yet
else
assert(false, 'unknown focus "'..Focus..'"')
end
add_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')
add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')
add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')
end
function add_hotkey_to_menu(s)
if Text_cache[s] == nil then
Text_cache[s] = App.newText(love.graphics.getFont(), s)
end
local width = App.width(Text_cache[s])
if Menu_cursor + width > App.screen.width - 5 then
return
end
App.color(Menu_command_color)
App.screen.draw(Text_cache[s], Menu_cursor,5)
Menu_cursor = Menu_cursor + width + 30
end
function source.draw_file_navigator()
for i,file in ipairs(File_navigation.candidates) do
if file == 'source' then
App.color(Menu_border_color)
love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)
end
add_file_to_menu(file, i == File_navigation.index)
end
end
function add_file_to_menu(s, cursor_highlight)
if Text_cache[s] == nil then
Text_cache[s] = App.newText(love.graphics.getFont(), s)
end
local width = App.width(Text_cache[s])
if Menu_cursor + width > App.screen.width - 5 then
return
end
if cursor_highlight then
App.color(Menu_highlight_color)
love.graphics.rectangle('fill', Menu_cursor-5,5-2, App.width(Text_cache[s])+5*2,Editor_state.line_height+2*2)
end
App.color(Menu_command_color)
App.screen.draw(Text_cache[s], Menu_cursor,5)
Menu_cursor = Menu_cursor + width + 30
end
function keychord_pressed_on_file_navigator(chord, key)
if chord == 'escape' then
Show_file_navigator = false
elseif chord == 'return' then
local candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')
source.switch_to_file(candidate)
Show_file_navigator = false
elseif chord == 'left' then
if File_navigation.index > 1 then
File_navigation.index = File_navigation.index-1
end
elseif chord == 'right' then
if File_navigation.index < #File_navigation.candidates then
File_navigation.index = File_navigation.index+1
end
end
end
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 tests
App.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 = 5
if Show_file_navigator then
source.draw_file_navigator()
return
end
add_hotkey_to_menu('ctrl+u: run')
if Focus == 'edit' then
add_hotkey_to_menu('ctrl+g: switch file')
if Show_log_browser_side then
add_hotkey_to_menu('ctrl+l: hide log browser')
else
add_hotkey_to_menu('ctrl+l: show log browser')
end
if Editor_state.expanded then
add_hotkey_to_menu('ctrl+b: collapse debug prints')
else
add_hotkey_to_menu('ctrl+b: expand debug prints')
end
add_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 yet
else
assert(false, 'unknown focus "'..Focus..'"')
end
add_hotkey_to_menu('ctrl+z ctrl+y: undo/redo')
add_hotkey_to_menu('ctrl+x ctrl+c ctrl+v: cut/copy/paste')
add_hotkey_to_menu('ctrl+= ctrl+- ctrl+0: zoom')
end
function add_hotkey_to_menu(s)
if Text_cache[s] == nil then
Text_cache[s] = App.newText(love.graphics.getFont(), s)
end
local width = App.width(Text_cache[s])
if Menu_cursor + width > App.screen.width - 5 then
return
end
App.color(Menu_command_color)
App.screen.draw(Text_cache[s], Menu_cursor,5)
Menu_cursor = Menu_cursor + width + 30
end
function source.draw_file_navigator()
for i,file in ipairs(File_navigation.candidates) do
if file == 'source' then
App.color(Menu_border_color)
love.graphics.line(Menu_cursor-10,2, Menu_cursor-10,Menu_status_bar_height-2)
end
add_file_to_menu(file, i == File_navigation.index)
end
end
function add_file_to_menu(s, cursor_highlight)
if Text_cache[s] == nil then
Text_cache[s] = App.newText(love.graphics.getFont(), s)
end
local width = App.width(Text_cache[s])
if Menu_cursor + width > App.screen.width - 5 then
return
end
if cursor_highlight then
App.color(Menu_highlight_color)
love.graphics.rectangle('fill', Menu_cursor-5,5-2, App.width(Text_cache[s])+5*2,Editor_state.line_height+2*2)
end
App.color(Menu_command_color)
App.screen.draw(Text_cache[s], Menu_cursor,5)
Menu_cursor = Menu_cursor + width + 30
end
function keychord_pressed_on_file_navigator(chord, key)
if chord == 'escape' then
Show_file_navigator = false
elseif chord == 'return' then
local candidate = guess_source(File_navigation.candidates[File_navigation.index]..'.lua')
source.switch_to_file(candidate)
Show_file_navigator = false
elseif chord == 'left' then
if File_navigation.index > 1 then
File_navigation.index = File_navigation.index-1
end
elseif chord == 'right' then
if File_navigation.index < #File_navigation.candidates then
File_navigation.index = File_navigation.index+1
end
end
end
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_color
Command_palette_border_color = Menu_border_color
Command_palette_command_color = Menu_command_color
Command_palette_alternatives_background_color = Menu_background_color
Command_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.love
Surface = {}
-- 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 surface
Column_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 surface
column_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 constants
Font_height = 20
Line_height = math.floor(Font_height*1.3)
-- space saved for headers
-- this is only on the screen, not used on the surface itself
Menu_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 surface
Padding_vertical = 20 -- space between panes
Padding_horizontal = 20
-- margins are extra space inside the borders of panes on the surface
Margin_above = 10
Margin_below = 10
Pan_step = 10
Pan = {}
Cursor_pane = {col=0, row=1} -- surface column and row index, along with some cached data
-- occasional secondary cursor
Grab_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 then
Editor_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) then
load_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' then
edit.fixup_cursor(Editor_state)
end
initialize_default_settings()
end
if Display_settings.column_width > App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal then
Display_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.filename
Editor_state.screen_top1 = Settings.screen_top
Editor_state.cursor1 = Settings.cursor
love.window.setPosition(settings.x, settings.y, settings.displayindex)
Font_height = settings.font_height
Line_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_width
for _,column_name in ipairs(settings.columns) do
create_column(column_name)
end
Cursor_pane.col = settings.cursor_col
Cursor_pane.row = settings.cursor_row
Display_settings.x = settings.surface_x
Display_settings.y = settings.surface_y
function run.initialize_default_settings()
local font_height = 20
love.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_height
Editor_state.line_height = math.floor(font_height*1.3)
Editor_state.em = em
Settings = 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 column
command.recently_modified()
Text.redraw_all(Editor_state)
Editor_state.selection1 = {} -- no support for shift drag while we're resizing
Editor_state.right = App.screen.width-Margin_right
Editor_state.width = Editor_state.right-Editor_state.left
Text.tweak_screen_top_and_cursor(Editor_state, Editor_state.left, Editor_state.right)
--? print('resize:', App.screen.width, App.screen.height)
plan_draw()
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)
end
function 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 = id
result.filename = Directory..id
result.lines = Cache[id].lines
result.line_cache = deepcopy(Cache[id].line_cache) -- should be tiny; deepcopy is just to eliminate any chance of aliasing
result.font_height = Font_height
result.line_height = Line_height
result.em = Em
result.editable = false
edit.fixup_cursor(result)
return result
function run.filedropped(file)
-- first make sure to save edits on any existing file
if Editor_state.next_save then
save_to_disk(Editor_state)
function height(pane)
if pane._height == nil then
refresh_pane_height(pane)
end
return pane._height
end
-- keep the structure of this function sync'd with plan_draw
function refresh_pane_height(pane)
--? print('refresh pane height')
local y = 0
if pane.title then
y = y + 5+Line_height+5
end
for i=1,#pane.lines do
local line = pane.lines[i]
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
if line.mode == 'text' then
pane.line_cache[i].fragments = nil
pane.line_cache[i].screen_line_starting_pos = nil
Text.compute_fragments(pane, i)
Text.populate_screen_line_starting_pos(pane, i)
y = y + Line_height*#pane.line_cache[i].screen_line_starting_pos
Text.clear_screen_line_cache(pane, i)
elseif line.mode == 'drawing' then
-- nothing
y = y + Drawing.pixels(line.h, Display_settings.column_width) + Drawing_padding_height
else
print(line.mode)
assert(false)
end
end
if Cache[pane.id].links and not empty(Cache[pane.id].links) then
y = y + 5+Line_height+5 -- for crosslinks
-- clear the slate for the new file
App.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 = y
end
-- titles are optional and so affect the height of the pane
function add_title(pane, title)
pane.title = title
pane._height = nil
end
-- keep the structure of this function sync'd with refresh_pane_height
function 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_left
for column_index, column in ipairs(Surface) do
if should_show_column(sx) then
table.insert(Column_headers_to_draw, {name=('%d. %s'):format(column_index, column.name), x = sx-Display_settings.x})
local sy = Padding_vertical
for pane_index, pane in ipairs(column) do
if sy > Display_settings.y + App.screen.height - Header_height then
break
end
--? print('bounds:', column_index, pane_index, sx,sy)
if should_show_pane(pane, sy) then
table.insert(Panes_to_draw, pane)
-- stash some short-lived variables
pane.column_index = column_index
pane.pane_index = pane_index
local y_offset = 0
local body_sy = sy
if column[pane_index].title then
body_sy = body_sy + 5+Line_height+5
end
if should_update_screen_top(column_index, pane_index, pane, options) then
if body_sy < Display_settings.y then
pane.screen_top1, y_offset = schema1_of_y(pane, Display_settings.y - body_sy)
else
pane.screen_top1 = {line=1, pos=1}
end
end
if body_sy < Display_settings.y then
pane.top = Margin_above
else
pane.top = body_sy - Display_settings.y + Margin_above
end
pane.top = Header_height + pane.top - y_offset
--? print('bounds: =>', pane.top)
pane.left = sx - Display_settings.x
pane.right = pane.left + Display_settings.column_width
pane.width = pane.right - pane.left
else
-- clear bounds to catch issues early
pane.top = nil
--? print('bounds: =>', pane.top)
end
sy = sy + Margin_above + height(pane) + Margin_below + Padding_vertical
end
else
-- clear bounds to catch issues early
for _, pane in ipairs(column) do
pane.top = nil
end
end
sx = sx + Margin_right + Display_settings.column_width + Padding_horizontal + Margin_left
end
function should_update_screen_top(column_index, pane_index, pane, options)
if column_index ~= Cursor_pane.col then return true end
if pane_index ~= Cursor_pane.row then return true end
-- update the cursor pane either if it's not editable, or
-- if it was explicitly requested
if not pane.editable then return true end
if options == nil then return true end
if not options.ignore_editable_cursor_pane then return true end
if not Editable_cursor_pane_updated_screen_top then return true end
return false
end
edit.draw(Editor_state)
--? print(Display_settings.y)
if Display_settings.mode == 'normal' then
draw_normal_mode()
elseif Display_settings.mode == 'search' then
draw_normal_mode()
-- hack: pass in an unexpected object and pun some attributes
Text.draw_search_bar(Display_settings, --[[force show cursor]] true)
elseif Display_settings.mode == 'search_all' then
draw_normal_mode()
-- only difference is in command palette below
elseif Display_settings.mode == 'searching_all' then
draw_normal_mode()
-- only difference is in command palette below
elseif Display_settings.mode == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
pane.top = Header_height + Margin_above
pane.left = App.screen.width/2 - 20*App.width(Em)
pane.right = App.screen.width/2 + 20*App.width(Em)
pane.width = pane.right - pane.left
edit.draw(pane)
end
end
else
print(Display_settings.mode)
assert(false)
end
if Grab_pane then
local old_top, old_left, old_right = Grab_pane.top, Grab_pane.left, Grab_pane.right
local old_screen_top = Grab_pane.screen_top1
Grab_pane.screen_top1 = {line=1, pos=1}
Grab_pane.top = App.screen.height - 10*Line_height
Grab_pane.left = App.screen.width - Display_settings.column_width - Margin_right - Padding_horizontal
Grab_pane.right = Grab_pane.left + Display_settings.column_width
Grab_pane.width = Grab_pane.right - Grab_pane.left
App.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_right
Grab_pane.screen_top1 = old_screen_top
end
draw_menu_bar()
if Display_settings.mode == 'search_all' or Display_settings.mode == 'searching_all' then
draw_command_palette_for_search_all()
elseif Display_settings.show_palette then
draw_command_palette()
end
end
function 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) do
assert(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)
--? end
if pane.title and eq(pane.screen_top1, {line=1, pos=1}) then
draw_title(pane)
end
edit.draw(pane)
if pane_drew_to_bottom(pane) then
draw_links(pane)
end
if pane.column_index == Cursor_pane.col and pane.pane_index == Cursor_pane.row then
App.color(Cursor_pane_background_color)
if pane.editable and Surface.cursor_on_screen_check then
assert(pane.cursor_y, 'cursor went off screen; this should never happen')
Surface.cursor_on_screen_check = false
end
else
App.color(Pane_background_color)
end
love.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)
end
for _,header in ipairs(Column_headers_to_draw) do
-- column header
App.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)
end
end
function pane_drew_to_bottom(pane)
return pane.bottom < App.screen.height - Line_height
end
function 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)
end
function 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)
end
function draw_title(pane)
assert(pane.title)
if Text_cache[pane.title] == nil then
Text_cache[pane.title] = App.newText(love.graphics.getFont(), pane.title)
end
App.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)
end
function draw_links(pane)
local links = Cache[pane.id].links
if links == nil then return end
if empty(links) then return end
local x = pane.left
for _,label in ipairs(Edge_list) do
if Text_cache[label] == nil then
Text_cache[label] = App.newText(love.graphics.getFont(), label)
end
if links[label] then
draw_link(label, x, pane.bottom)
end
x = x + App.width(Text_cache[label]) + 10 + 10
end
-- links we don't know about, just in case
for link,_ in pairs(links) do
if not Opposite[link] then
if Text_cache[link] == nil then
Text_cache[link] = App.newText(love.graphics.getFont(), link)
end
draw_link(link, x, pane.bottom)
x = x + App.width(Text_cache[link]) + 10 + 10
end
end
pane.bottom = pane.bottom + 5+Line_height+5
end
function 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 then
return true
end
-- lo2 hi2
-- | |
-- | |
if lo1 < hi2 and hi1 >= hi2 then
return true
end
-- lo2 hi2
-- | |
-- | |
return lo1 >= lo2 and hi1 <= hi2
edit.update(Editor_state, dt)
if App.mouse_y() < Header_height then
-- column header
love.mouse.setCursor(love.mouse.getSystemCursor('arrow'))
elseif in_pane(App.mouse_x(), App.mouse_y()) then
love.mouse.setCursor(love.mouse.getSystemCursor('arrow'))
else
love.mouse.setCursor(love.mouse.getSystemCursor('hand'))
end
if Pan.x then
Display_settings.x = math.max(Pan.x-App.mouse_x(), 0)
Display_settings.y = math.max(Pan.y-(App.mouse_y()-Header_height), 0)
end
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
edit.update(pane, dt)
end
end
if 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 dragging
plan_draw()
end
if Display_settings.mode == 'searching_all' then
resume_search_all()
end
end
function in_pane(x,y)
-- duplicate some logic from App.draw
local sx,sy = to_surface(x,y)
local x = Padding_horizontal
for column_idx, column in ipairs(Surface) do
if sx < x then
return false
end
if sx < x + Margin_left + Display_settings.column_width + Margin_right then
local y = Padding_vertical
for pane_idx, pane in ipairs(column) do
if sy < y then
return false
end
if sy < y + Margin_above + height(pane) + Margin_below then
return true
end
y = y + Margin_above + height(pane) + Margin_below + Padding_vertical
end
end
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
end
return false
end
function to_pane(sx,sy)
-- duplicate some logic from App.draw
local x = Padding_horizontal
for column_idx, column in ipairs(Surface) do
if sx < x then
return nil
end
if sx < x + Margin_left + Display_settings.column_width + Margin_right then
local y = Padding_vertical
for pane_idx, pane in ipairs(column) do
if sy < y then
return nil
end
if sy < y + Margin_above + height(pane) + Margin_below then
return {col=column_idx, row=pane_idx}
end
y = y + Margin_above + height(pane) + Margin_below + Padding_vertical
end
end
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
end
return nil
local filename = Editor_state.filename
if filename:sub(1,1) ~= '/' then
filename = love.filesystem.getWorkingDirectory()..'/'..filename -- '/' should work even on Windows
local column_names = {}
for _,column in ipairs(Surface) do
table.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' then
mouse_pressed_in_normal_mode(x,y, mouse_button)
elseif Display_settings.mode == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_pressed(pane, x,y, mouse_button)
end
end
else
print(Display_settings.mode)
assert(false)
end
end
function clear_selections()
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane.selection1 = {}
end
end
end
function mouse_pressed_in_normal_mode(x,y, mouse_button)
Pan = {}
if y < Header_height then
-- column headers currently not interactable
return
end
local 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 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_pressed(pane, x,y, mouse_button)
pane._height = nil
end
end
else
Pan = {x=sx, y=sy}
end
return edit.textinput(Editor_state, t)
--? print('textinput', t)
-- hotkeys operating on the cursor pane
if Display_settings.show_palette then
Display_settings.palette_command = Display_settings.palette_command..t
Display_settings.palette_command_text = App.newText(love.graphics.getFont(), Display_settings.palette_command)
Display_settings.palette_alternative_index = 1
Display_settings.palette_candidates = candidates()
elseif Display_settings.mode == 'normal' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if not pane.editable then
-- global hotkeys for normal mode
if t == 'X' then
command.wider_columns()
return
elseif t == 'x' then
command.narrower_columns()
return
end
-- send keys to the current pane
else
if pane.cursor_x >= 0 and pane.cursor_x < App.screen.width then
if 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 = nil
plan_draw()
end
end
end
end
end
elseif Display_settings.mode == 'search' then
--? print('insert', t)
Display_settings.search_term = Display_settings.search_term..t
Display_settings.search_text = nil
-- reset search state
clear_selections()
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
elseif Display_settings.mode == 'search_all' then
Display_settings.search_all_query = Display_settings.search_all_query..t
Display_settings.search_all_query_text = nil
elseif Display_settings.mode == 'searching_all' then
Display_settings.mode = 'normal'
Display_settings.search_all_query_text = nil
elseif Display_settings.mode == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if pane.editable then
edit.textinput(pane, t)
end
end
end
else
print(Display_settings.mode)
assert(false)
end
return edit.keychord_pressed(Editor_state, chord, key)
-- global hotkeys
if chord == 'C-=' then
update_font_settings(Font_height+2)
elseif chord == 'C--' then
update_font_settings(Font_height-2)
elseif chord == 'C-0' then
update_font_settings(20)
-- mode-specific hotkeys
elseif Display_settings.show_palette then
keychord_pressed_on_command_palette(chord, key)
elseif Display_settings.mode == 'normal' then
if chord == 'C-return' then
Display_settings.show_palette = true
Display_settings.palette_candidates = candidates()
elseif chord == 'C-f' then
command.commence_find_on_surface()
elseif Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
keychord_pressed_on_editable_pane(pane, chord, key)
else
keychord_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 here
plan_draw{ignore_editable_cursor_pane=true}
end
elseif Display_settings.mode == 'search' then
keychord_pressed_in_search_mode(chord, key)
elseif Display_settings.mode == 'search_all' then
keychord_pressed_in_search_all_mode(chord, key)
elseif Display_settings.mode == 'searching_all' then
interrupt_search_all()
elseif Display_settings.mode == 'maximize' then
if chord == 'C-return' then
Display_settings.show_palette = true
Display_settings.palette_candidates = candidates()
else
keychord_pressed_in_maximize_mode(chord, key)
end
else
print(Display_settings.mode)
assert(false)
end
end
function update_font_settings(font_height)
local column_width_in_ems = Display_settings.column_width / App.width(Em)
Font_height = font_height
love.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) do
for _,pane in ipairs(column) do
pane.font_height = Font_height
pane.line_height = Line_height
pane.em = Em
pane.left = 0
pane.right = Display_settings.column_width
end
end
clear_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 end
clear_all_search_terms()
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
--? 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 cursor
if search_next_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) then
--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)
return
end
pane.cursor1 = old_cursor_in_cursor_pane
-- scan current column down from current pane
for current_pane_index=Cursor_pane.row+1,#Surface[Cursor_pane.col] do
local 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) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
local current_column_index = 1 + Cursor_pane.col%#Surface -- (i+1)%#Surface in the presence of 1-indexing
-- scan columns past current, looping around
while true do
for current_pane_index,pane in ipairs(Surface[current_column_index]) do
pane.cursor1 = {line=1, pos=1}
edit.fixup_cursor(pane)
pane.screen_top1 = {line=1, pos=1}
if search_next_in_pane(pane) then
Cursor_pane = {col=current_column_index, row=current_pane_index}
--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- loop update
current_column_index = 1 + current_column_index%#Surface -- i = (i+1)%#Surface in the presence of 1-indexing
-- termination check
if current_column_index == Cursor_pane.col then
break
end
end
-- scan current column until current pane
for current_pane_index=1,Cursor_pane.row-1 do
if search_next_in_pane(Surface[Cursor_pane.col][current_pane_index]) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- finally, scan the cursor pane until the cursor
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
local old_cursor = pane.cursor1
pane.cursor1 = {line=1, pos=1}
edit.fixup_cursor(pane)
pane.screen_top1 = {line=1, pos=1}
if search_next_in_pane(pane) then
if Text.lt1(pane.cursor1, old_cursor) then
return
end
end
-- nothing found
pane.cursor1 = old_cursor_in_cursor_pane
end
-- returns whether it found an occurrence
function search_next_in_pane(pane)
pane.search_term = Display_settings.search_term
pane.search_text = Display_settings.search_text
pane.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 do
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
end
if Text.search_next(pane) then
if Text.le1(pane.search_backup.cursor, pane.cursor1) then
-- select this occurrence
return true
end
-- Otherwise cursor wrapped around. Skip this pane.
end
-- Clean up this pane before moving on to the next one.
pane.search_term = nil
pane.search_text = nil
pane.cursor1.line = pane.search_backup.cursor.line
pane.cursor1.pos = pane.search_backup.cursor.pos
pane.screen_top1.line = pane.search_backup.screen_top.line
pane.screen_top1.pos = pane.search_backup.screen_top.pos
pane.search_backup = nil
end
-- Scan all panes, while delegating as much work as possible to lines.love search.
function search_previous()
if Cursor_pane.col < 1 then return end
clear_all_search_terms()
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
--? 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 cursor
if search_previous_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) then
--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)
return
end
pane.cursor1 = old_cursor_in_cursor_pane
-- scan current column down from current pane
for current_pane_index=Cursor_pane.row-1,1,-1 do
local pane = Surface[Cursor_pane.col][current_pane_index]
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
local current_column_index = 1 + (Cursor_pane.col-2)%#Surface -- (i-1)%#Surface in the presence of 1-indexing
-- scan columns past current, looping around
while true do
for current_pane_index = #Surface[current_column_index],1,-1 do
local pane = Surface[current_column_index][current_pane_index]
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
Cursor_pane = {col=current_column_index, row=current_pane_index}
--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- loop update
current_column_index = 1 + (current_column_index-2)%#Surface -- i = (i-1)%#Surface in the presence of 1-indexing
-- termination check
if current_column_index == Cursor_pane.col then
break
end
end
-- scan current column from bottom current pane
for 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]) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- finally, scan the cursor pane from bottom until cursor
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
local old_cursor = pane.cursor1
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
if Text.lt1(old_cursor, pane.cursor1) then
return
end
end
-- nothing found
pane.cursor1 = old_cursor_in_cursor_pane
end
-- returns whether it found an occurrence
function search_previous_in_pane(pane)
pane.search_term = Display_settings.search_term
pane.search_text = Display_settings.search_text
pane.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 do
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
end
if Text.search_previous(pane) then
if Text.lt1(pane.cursor1, pane.search_backup.cursor) then
-- select this occurrence
return true
end
-- Otherwise cursor wrapped around. Skip this pane.
end
-- Clean up this pane before moving on to the previous one.
pane.search_term = nil
pane.search_text = nil
pane.cursor1.line = pane.search_backup.cursor.line
pane.cursor1.pos = pane.search_backup.cursor.pos
pane.screen_top1.line = pane.search_backup.screen_top.line
pane.screen_top1.pos = pane.search_backup.screen_top.pos
pane.search_backup = nil
end
function bring_cursor_of_cursor_pane_in_view(dir)
if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
--? 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 bottom
if vertically_ok and horizontally_ok then
return
end
if dir == 'up' then
if not vertically_ok then
Display_settings.x = left_edge_sx - Margin_left - Padding_horizontal
end
if not horizontally_ok then
Display_settings.y = cursor_sy - 3*Line_height
end
else
assert(dir == 'down')
if not vertically_ok then
Display_settings.x = left_edge_sx + Display_settings.column_width + Margin_right + Padding_horizontal - App.screen.width
end
if 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 wrong
assert(App.screen.height - (cursor_sy-Display_settings.y) > 1.5*Line_height)
end
end
--? 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)
end
function clear_all_search_terms()
for col,column in ipairs(Surface) do
for row,pane in ipairs(column) do
pane.search_term = nil
pane.search_text = nil
end
end
end
function keychord_pressed_in_maximize_mode(chord, key)
if Cursor_pane.col < 1 then
print('no current note to edit')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
print('no current note to edit')
return
end
if pane.editable then
if chord == 'C-e' then
command.exit_editing()
else
edit.keychord_pressed(pane, chord, key)
end
else
if chord == 'C-e' then
command.edit_note()
elseif chord == 'C-c' then
edit.keychord_pressed(pane, chord, key)
end
end
end
function keychord_pressed_on_editable_pane(pane, chord, key)
-- ignore if cursor is not visible on screen
if pane.cursor_x == nil then
assert(pane.cursor_y == nil)
panning_keychord_pressed(chord, key)
return
end
if chord == 'C-e' then
command.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 = nil
end
end
function 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 pane
pane.screen_top1 = old_top
bring_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 after
return
end
Editable_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 after
end
end
function keychord_pressed_in_normal_mode_with_immutable_pane(pane, chord, key)
-- return if no part of cursor pane is visible
local left_sx = left_edge_sx(Cursor_pane.col)
if not should_show_column(left_sx) then
panning_keychord_pressed(chord, key)
return
end
local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)
if not should_show_pane(pane, up_sy) then
panning_keychord_pressed(chord, key)
return
end
if chord == 'C-e' then
command.edit_note()
elseif chord == 'C-c' then
edit.keychord_pressed(pane, chord, key)
else
panning_keychord_pressed(chord, key)
end
end
-- 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 = 0
if pane.title then
result = result + 5+Line_height+5
end
result = result + Margin_above
if loc.line == 1 and loc.pos == 1 then
return result
end
for 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)
end
if pane.lines[loc.line].mode == 'text' then
Text.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) do
if screen_line_starting_pos >= loc.pos then
break
end
result = result + Line_height
end
end
--? print(('viewport at %d'):format(result))
return result
end
function keychord_pressed_in_search_mode(chord, key)
if chord == 'escape' then
Display_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_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- don't forget search text
elseif chord == 'return' then
Display_settings.mode = 'normal'
clear_all_search_terms()
clean_up_panes()
-- forget old viewport
--? print('return; exiting search mode')
Display_settings.search_backup_x = nil
Display_settings.search_backup_y = nil
Display_settings.search_backup_cursor_pane = nil
-- don't forget search text
elseif chord == 'backspace' then
local 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 state
clear_selections()
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
--? print('backspace; search term is now', Display_settings.search_term)
elseif chord == 'C-v' then
Display_settings.search_term = Display_settings.search_term..App.getClipboardText()
Display_settings.search_text = nil
-- reset search state
clear_selections()
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
--? print('paste; search term is now', Display_settings.search_term)
elseif chord == 'up' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
search_previous()
bring_cursor_of_cursor_pane_in_view('up')
Surface.cursor_on_screen_check = true
plan_draw()
end
end
elseif chord == 'down' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
pane.cursor1.pos = pane.cursor1.pos+1
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
end
end
-- things from normal mode we still want
elseif chord == 'C-c' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.keychord_pressed(pane, chord, key)
end
end
end
end
function keychord_pressed_in_search_all_mode(chord, key)
if chord == 'escape' then
Display_settings.mode = 'normal'
-- don't forget search text
Display_settings.search_all_state = nil
elseif chord == 'return' then
finalize_search_all_pane()
add_search_all_pane_to_right_of_cursor()
Display_settings.mode = 'searching_all'
plan_draw()
elseif chord == 'backspace' then
local 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' then
Display_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)
end
end
-- 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 = y
for 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 then
local line = pane.lines[i]
if line.mode ~= 'text' then
return {line=i, pos=1}, y_offset
else
local 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-indexing
y_offset = y_offset - nlines*pane.line_height
return {line=i, pos=pos}, y_offset
end
end
y_offset = y_offset - height
end
-- y is below the pane
return {line=#pane.lines+1, pos=1}, y_offset
end
function 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' then
return Line_height*#line_cache.screen_line_starting_pos
else
return Drawing.pixels(line.h, right-left) + Drawing_padding_height
end
end
function stop_editing_all()
local edit_count = 0
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
if pane.editable then
stop_editing(pane)
edit_count = edit_count+1
end
end
end
assert(edit_count <= 1)
end
function stop_editing(pane)
edit.quit(pane)
-- save symmetric links
for rel,target in pairs(Cache[pane.id].links) do
initialize_cache_if_necessary(target)
save_links(target)
end
if Display_settings.mode ~= 'maximize' then
refresh_panes(pane)
end
pane.editable = false
end
function panning_keychord_pressed(chord, key)
if chord == 'up' then
Display_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.y
if up_py > 2/3*App.screen.height then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
elseif chord == 'down' then
local visible_column_max_y = most(column_height, visible_columns())
if visible_column_max_y - Display_settings.y > App.screen.height/2 then
Display_settings.y = Display_settings.y + Pan_step
end
local down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)
local down_px = down_sx - Display_settings.y
if down_px < App.screen.height/3 then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
elseif chord == 'left' then
Display_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.x
if left_px > App.screen.width - Margin_right - Display_settings.column_width/2 then
Cursor_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)
end
elseif chord == 'right' then
if Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) then
Display_settings.x = Display_settings.x + Pan_step
end
local right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_width
local right_px = right_sx - Display_settings.x
if right_px < Margin_left + Display_settings.column_width/2 then
Cursor_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)
end
elseif chord == 'pageup' or chord == 'S-up' then
Display_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.y
if up_py > 2/3*App.screen.height then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
elseif 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*2
end
local down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)
local down_px = down_sx - Display_settings.y
if 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)
end
elseif chord == 'S-left' then
Display_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.x
if left_px > App.screen.width - Margin_right - Display_settings.column_width/2 then
Cursor_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)
end
elseif chord == 'S-right' then
if Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) then
Display_settings.x = Display_settings.x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
local right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_width
local right_px = right_sx - Display_settings.x
if right_px < Margin_left + Display_settings.column_width/2 then
Cursor_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)
end
end
elseif chord == 'C-down' then
command.down_one_pane()
elseif chord == 'C-up' then
command.up_one_pane()
elseif chord == 'C-end' then
command.bottom_pane_of_column()
elseif chord == 'C-home' then
command.top_pane_of_column()
end
--? print('after', Cursor_pane.col, Cursor_pane.row)
end
function visible_columns()
local result = {}
local col = col(Display_settings.x)
local x = left_edge_sx(col) - Display_settings.x
while col <= #Surface do
x = x + Padding_horizontal
table.insert(result, col)
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
if x > App.screen.width then
break
end
col = col+1
end
return result
end
function refresh_panes(pane)
--? print('refreshing')
Cache[pane.id].lines = pane.lines
for x,col in ipairs(Surface) do
for y,p in ipairs(col) do
if p.id == pane.id then
--? print(x,y)
p.lines = pane.lines
p._height = nil
Text.redraw_all(p)
end
end
end
plan_draw()
return edit.key_released(Editor_state, key, scancode)
if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
edit.key_released(pane, key, scancode)
end
end
function clear_all_pane_heights()
Text_cache = {}
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane._height = nil
end
end
end
-- convert x surface pixel coordinate into column index
function 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 col
function left_edge_sx(col)
return (col-1)*(Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) + Padding_horizontal + Margin_left
end
function row(col, y)
local sy = Padding_vertical
for i,pane in ipairs(Surface[col]) do
--? print('', i, y, sy, next_sy)
local next_sy = sy + Margin_above + height(pane) + Margin_below + Padding_vertical
if next_sy > y then
return i
end
sy = next_sy
end
return #Surface[col]
end
function up_edge_sy(col, row)
local result = Padding_vertical
for i=1,row-1 do
local pane = Surface[col][i]
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result
end
function down_edge_sx(col, row)
local result = Padding_vertical
for i=1,row do
local pane = Surface[col][i]
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result - Padding_vertical
end
function column_height(col)
local result = Padding_vertical
for pane_index, pane in ipairs(Surface[col]) do
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result
end
function most(f, arr)
local result = nil
for _,x in ipairs(arr) do
local curr = f(x)
if result == nil or result < curr then
result = curr
end
end
return 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_color
Command_palette_border_color = Menu_border_color
Command_palette_command_color = Menu_command_color
Command_palette_alternatives_background_color = Menu_background_color
Command_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.love
Surface = {}
-- 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 surface
Column_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 surface
column_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 constants
Font_height = 20
Line_height = math.floor(Font_height*1.3)
-- space saved for headers
-- this is only on the screen, not used on the surface itself
Menu_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 surface
Padding_vertical = 20 -- space between panes
Padding_horizontal = 20
-- margins are extra space inside the borders of panes on the surface
Margin_above = 10
Margin_below = 10
Pan_step = 10
Pan = {}
Cursor_pane = {col=0, row=1} -- surface column and row index, along with some cached data
-- occasional secondary cursor
Grab_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 changes
Text_cache = {}
assert(#arg <= 1)
if #arg > 0 then
Directory = 'data.'..arg[1]..'/'
Settings_file = 'config.'..arg[1]
if Current_app == 'run' then
load_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')
else
load_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 sometimes
function App.load()
if love.filesystem.getInfo(Settings_file) then
load_settings()
function App.initialize_globals()
if Current_app == 'run' then
run.initialize_globals()
elseif Current_app == 'source' then
source.initialize_globals()
if Display_settings.column_width > App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal then
Display_settings.column_width = math.max(200, App.screen.width - Padding_horizontal - Margin_left - Margin_right - Padding_horizontal)
-- for hysteresis in a few places
Last_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700
Last_resize_time = App.getTime()
end
function App.initialize(arg)
if Current_app == 'run' then
run.initialize(arg)
elseif Current_app == 'source' then
source.initialize(arg)
else
assert(false, 'unknown app "'..Current_app..'"')
Settings = json.decode(love.filesystem.read('config'))
Current_app = Settings.current_app
if Current_app == nil then
Current_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' then
if run.resize then run.resize(w,h) end
elseif Current_app == 'source' then
if source.resize then source.resize(w,h) end
else
assert(false, 'unknown app "'..Current_app..'"')
end
Last_resize_time = App.getTime()
function App.filedropped(file)
if Current_app == 'run' then
if run.filedropped then run.filedropped(file) end
elseif Current_app == 'source' then
if source.filedropped then source.filedropped(file) end
else
assert(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 then
Last_focus_time = App.getTime()
if Current_app == 'run' then
if run.focus then run.focus(in_focus) end
elseif Current_app == 'source' then
if source.focus then source.focus(in_focus) end
else
assert(false, 'unknown app "'..Current_app..'"')
end
--
if Current_app == 'run' then
run.update(dt)
elseif Current_app == 'source' then
source.update(dt)
else
assert(false, 'unknown app "'..Current_app..'"')
end
function App.draw()
if Current_app == 'run' then
run.draw()
elseif Current_app == 'source' then
source.draw()
else
assert(false, 'unknown app "'..Current_app..'"')
end
end
function App.update(dt)
-- some hysteresis while resizing
if App.getTime() < Last_resize_time + 0.1 then
return
end
end
end
function 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 = id
result.filename = Directory..id
result.lines = Cache[id].lines
result.line_cache = deepcopy(Cache[id].line_cache) -- should be tiny; deepcopy is just to eliminate any chance of aliasing
result.font_height = Font_height
result.line_height = Line_height
result.em = Em
result.editable = false
edit.fixup_cursor(result)
return result
end
function height(pane)
if pane._height == nil then
refresh_pane_height(pane)
-- keep the structure of this function sync'd with plan_draw
function refresh_pane_height(pane)
--? print('refresh pane height')
local y = 0
if pane.title then
y = y + 5+Line_height+5
end
for i=1,#pane.lines do
local line = pane.lines[i]
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
if line.mode == 'text' then
pane.line_cache[i].fragments = nil
pane.line_cache[i].screen_line_starting_pos = nil
Text.compute_fragments(pane, i)
Text.populate_screen_line_starting_pos(pane, i)
y = y + Line_height*#pane.line_cache[i].screen_line_starting_pos
Text.clear_screen_line_cache(pane, i)
elseif line.mode == 'drawing' then
-- nothing
y = y + Drawing.pixels(line.h, Display_settings.column_width) + Drawing_padding_height
else
print(line.mode)
assert(false)
end
end
if Cache[pane.id].links and not empty(Cache[pane.id].links) then
y = y + 5+Line_height+5 -- for crosslinks
end
pane._height = y
-- titles are optional and so affect the height of the pane
function add_title(pane, title)
pane.title = title
pane._height = nil
end
-- keep the structure of this function sync'd with refresh_pane_height
function 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_left
for column_index, column in ipairs(Surface) do
if should_show_column(sx) then
table.insert(Column_headers_to_draw, {name=('%d. %s'):format(column_index, column.name), x = sx-Display_settings.x})
local sy = Padding_vertical
for pane_index, pane in ipairs(column) do
if sy > Display_settings.y + App.screen.height - Header_height then
break
end
--? print('bounds:', column_index, pane_index, sx,sy)
if should_show_pane(pane, sy) then
table.insert(Panes_to_draw, pane)
-- stash some short-lived variables
pane.column_index = column_index
pane.pane_index = pane_index
local y_offset = 0
local body_sy = sy
if column[pane_index].title then
body_sy = body_sy + 5+Line_height+5
end
if should_update_screen_top(column_index, pane_index, pane, options) then
if body_sy < Display_settings.y then
pane.screen_top1, y_offset = schema1_of_y(pane, Display_settings.y - body_sy)
else
pane.screen_top1 = {line=1, pos=1}
end
end
if body_sy < Display_settings.y then
pane.top = Margin_above
else
pane.top = body_sy - Display_settings.y + Margin_above
end
pane.top = Header_height + pane.top - y_offset
--? print('bounds: =>', pane.top)
pane.left = sx - Display_settings.x
pane.right = pane.left + Display_settings.column_width
pane.width = pane.right - pane.left
else
-- clear bounds to catch issues early
pane.top = nil
--? print('bounds: =>', pane.top)
end
sy = sy + Margin_above + height(pane) + Margin_below + Padding_vertical
end
else
-- clear bounds to catch issues early
for _, pane in ipairs(column) do
pane.top = nil
end
end
sx = sx + Margin_right + Display_settings.column_width + Padding_horizontal + Margin_left
end
function should_update_screen_top(column_index, pane_index, pane, options)
if column_index ~= Cursor_pane.col then return true end
if pane_index ~= Cursor_pane.row then return true end
-- update the cursor pane either if it's not editable, or
-- if it was explicitly requested
if not pane.editable then return true end
if options == nil then return true end
if not options.ignore_editable_cursor_pane then return true end
if not Editable_cursor_pane_updated_screen_top then return true end
return false
end
function App.draw()
--? print(Display_settings.y)
if Display_settings.mode == 'normal' then
draw_normal_mode()
elseif Display_settings.mode == 'search' then
draw_normal_mode()
-- hack: pass in an unexpected object and pun some attributes
Text.draw_search_bar(Display_settings, --[[force show cursor]] true)
elseif Display_settings.mode == 'search_all' then
draw_normal_mode()
-- only difference is in command palette below
elseif Display_settings.mode == 'searching_all' then
draw_normal_mode()
-- only difference is in command palette below
elseif Display_settings.mode == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
pane.top = Header_height + Margin_above
pane.left = App.screen.width/2 - 20*App.width(Em)
pane.right = App.screen.width/2 + 20*App.width(Em)
pane.width = pane.right - pane.left
edit.draw(pane)
end
end
else
print(Display_settings.mode)
assert(false)
end
if Grab_pane then
local old_top, old_left, old_right = Grab_pane.top, Grab_pane.left, Grab_pane.right
local old_screen_top = Grab_pane.screen_top1
Grab_pane.screen_top1 = {line=1, pos=1}
Grab_pane.top = App.screen.height - 10*Line_height
Grab_pane.left = App.screen.width - Display_settings.column_width - Margin_right - Padding_horizontal
Grab_pane.right = Grab_pane.left + Display_settings.column_width
Grab_pane.width = Grab_pane.right - Grab_pane.left
App.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_right
Grab_pane.screen_top1 = old_screen_top
end
draw_menu_bar()
if Display_settings.mode == 'search_all' or Display_settings.mode == 'searching_all' then
draw_command_palette_for_search_all()
elseif Display_settings.show_palette then
draw_command_palette()
end
end
function 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) do
assert(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)
--? end
if pane.title and eq(pane.screen_top1, {line=1, pos=1}) then
draw_title(pane)
end
edit.draw(pane)
if pane_drew_to_bottom(pane) then
draw_links(pane)
end
if pane.column_index == Cursor_pane.col and pane.pane_index == Cursor_pane.row then
App.color(Cursor_pane_background_color)
if pane.editable and Surface.cursor_on_screen_check then
assert(pane.cursor_y, 'cursor went off screen; this should never happen')
Surface.cursor_on_screen_check = false
end
else
App.color(Pane_background_color)
end
love.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)
end
for _,header in ipairs(Column_headers_to_draw) do
-- column header
App.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)
end
end
function pane_drew_to_bottom(pane)
return pane.bottom < App.screen.height - Line_height
end
function 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)
end
function 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)
end
function draw_title(pane)
assert(pane.title)
if Text_cache[pane.title] == nil then
Text_cache[pane.title] = App.newText(love.graphics.getFont(), pane.title)
end
App.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].links
if links == nil then return end
if empty(links) then return end
local x = pane.left
for _,label in ipairs(Edge_list) do
if Text_cache[label] == nil then
Text_cache[label] = App.newText(love.graphics.getFont(), label)
end
if links[label] then
draw_link(label, x, pane.bottom)
end
x = x + App.width(Text_cache[label]) + 10 + 10
end
-- links we don't know about, just in case
for link,_ in pairs(links) do
if not Opposite[link] then
if Text_cache[link] == nil then
Text_cache[link] = App.newText(love.graphics.getFont(), link)
end
draw_link(link, x, pane.bottom)
x = x + App.width(Text_cache[link]) + 10 + 10
end
end
pane.bottom = pane.bottom + 5+Line_height+5
end
function 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 then
return true
end
-- lo2 hi2
-- | |
-- | |
if lo1 < hi2 and hi1 >= hi2 then
return true
end
-- lo2 hi2
-- | |
-- | |
return lo1 >= lo2 and hi1 <= hi2
function App.update(dt)
Cursor_time = Cursor_time + dt
-- some hysteresis while resizing
if App.getTime() < Last_resize_time + 0.1 then
return
end
if App.mouse_y() < Header_height then
-- column header
love.mouse.setCursor(love.mouse.getSystemCursor('arrow'))
elseif in_pane(App.mouse_x(), App.mouse_y()) then
love.mouse.setCursor(love.mouse.getSystemCursor('arrow'))
else
love.mouse.setCursor(love.mouse.getSystemCursor('hand'))
end
if Pan.x then
Display_settings.x = math.max(Pan.x-App.mouse_x(), 0)
Display_settings.y = math.max(Pan.y-(App.mouse_y()-Header_height), 0)
end
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
edit.update(pane, dt)
end
end
if 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 dragging
plan_draw()
end
if Display_settings.mode == 'searching_all' then
resume_search_all()
end
end
function in_pane(x,y)
-- duplicate some logic from App.draw
local sx,sy = to_surface(x,y)
local x = Padding_horizontal
for column_idx, column in ipairs(Surface) do
if sx < x then
return false
end
if sx < x + Margin_left + Display_settings.column_width + Margin_right then
local y = Padding_vertical
for pane_idx, pane in ipairs(column) do
if sy < y then
return false
end
if sy < y + Margin_above + height(pane) + Margin_below then
return true
end
y = y + Margin_above + height(pane) + Margin_below + Padding_vertical
end
end
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
end
return false
end
function to_pane(sx,sy)
-- duplicate some logic from App.draw
local x = Padding_horizontal
for column_idx, column in ipairs(Surface) do
if sx < x then
return nil
end
if sx < x + Margin_left + Display_settings.column_width + Margin_right then
local y = Padding_vertical
for pane_idx, pane in ipairs(column) do
if sy < y then
return nil
end
if sy < y + Margin_above + height(pane) + Margin_below then
return {col=column_idx, row=pane_idx}
end
y = y + Margin_above + height(pane) + Margin_below + Padding_vertical
end
end
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
end
return nil
end
function to_surface(x, y)
return x+Display_settings.x, y+Display_settings.y-Header_height
end
function love.quit()
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
edit.quit(pane)
end
end
-- save some important settings
local x,y,displayindex = love.window.getPosition()
local column_names = {}
for _,column in ipairs(Surface) do
table.insert(column_names, column.name)
end
local 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))
end
function App.mousepressed(x,y, mouse_button)
--? print('app mouse pressed', x,y)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
clear_selections()
if Display_settings.mode == 'normal' or Display_settings.mode == 'search' or Display_settings.mode == 'search_all' or Display_settings.mode == 'searching_all' then
mouse_pressed_in_normal_mode(x,y, mouse_button)
elseif Display_settings.mode == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_pressed(pane, x,y, mouse_button)
end
end
else
print(Display_settings.mode)
assert(false)
end
end
function clear_selections()
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane.selection1 = {}
end
end
end
function mouse_pressed_in_normal_mode(x,y, mouse_button)
Pan = {}
if y < Header_height then
-- column headers currently not interactable
return
end
local 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 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.mouse_pressed(pane, x,y, mouse_button)
pane._height = nil
end
end
if Current_app == 'run' then
if run.keychord_pressed then run.keychord_pressed(chord, key) end
elseif Current_app == 'source' then
if source.keychord_pressed then source.keychord_pressed(chord, key) end
Pan = {x=sx, y=sy}
end
end
function App.mousereleased(x,y, mouse_button)
--? print('app mouse released')
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Cursor_pane.col >= 1 then
edit.mouse_released(Surface[Cursor_pane.col][Cursor_pane.row], x,y, mouse_button)
end
Pan = {}
end
function App.focus(in_focus)
if in_focus then
Last_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 pane
if Display_settings.show_palette then
Display_settings.palette_command = Display_settings.palette_command..t
Display_settings.palette_command_text = App.newText(love.graphics.getFont(), Display_settings.palette_command)
Display_settings.palette_alternative_index = 1
Display_settings.palette_candidates = candidates()
elseif Display_settings.mode == 'normal' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if not pane.editable then
-- global hotkeys for normal mode
if t == 'X' then
command.wider_columns()
return
elseif t == 'x' then
command.narrower_columns()
return
end
-- send keys to the current pane
else
if pane.cursor_x >= 0 and pane.cursor_x < App.screen.width then
if 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 = nil
plan_draw()
end
end
end
end
end
elseif Display_settings.mode == 'search' then
--? print('insert', t)
Display_settings.search_term = Display_settings.search_term..t
Display_settings.search_text = nil
-- reset search state
clear_selections()
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
elseif Display_settings.mode == 'search_all' then
Display_settings.search_all_query = Display_settings.search_all_query..t
Display_settings.search_all_query_text = nil
elseif Display_settings.mode == 'searching_all' then
Display_settings.mode = 'normal'
Display_settings.search_all_query_text = nil
elseif Display_settings.mode == 'maximize' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
if pane.editable then
edit.textinput(pane, t)
end
end
end
else
print(Display_settings.mode)
assert(false)
end
end
function App.keychord_pressed(chord, key)
-- ignore events for some time after window in focus
if App.getTime() < Last_focus_time + 0.01 then
return
end
--? print('keychord press', chord)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
-- global hotkeys
if chord == 'C-=' then
update_font_settings(Font_height+2)
elseif chord == 'C--' then
update_font_settings(Font_height-2)
elseif chord == 'C-0' then
update_font_settings(20)
-- mode-specific hotkeys
elseif Display_settings.show_palette then
keychord_pressed_on_command_palette(chord, key)
elseif Display_settings.mode == 'normal' then
if chord == 'C-return' then
Display_settings.show_palette = true
Display_settings.palette_candidates = candidates()
elseif chord == 'C-f' then
command.commence_find_on_surface()
elseif Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
keychord_pressed_on_editable_pane(pane, chord, key)
else
keychord_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 here
plan_draw{ignore_editable_cursor_pane=true}
end
elseif Display_settings.mode == 'search' then
keychord_pressed_in_search_mode(chord, key)
elseif Display_settings.mode == 'search_all' then
keychord_pressed_in_search_all_mode(chord, key)
elseif Display_settings.mode == 'searching_all' then
interrupt_search_all()
elseif Display_settings.mode == 'maximize' then
if chord == 'C-return' then
Display_settings.show_palette = true
Display_settings.palette_candidates = candidates()
else
keychord_pressed_in_maximize_mode(chord, key)
end
else
print(Display_settings.mode)
assert(false)
end
end
function update_font_settings(font_height)
local column_width_in_ems = Display_settings.column_width / App.width(Em)
Font_height = font_height
love.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) do
for _,pane in ipairs(column) do
pane.font_height = Font_height
pane.line_height = Line_height
pane.em = Em
pane.left = 0
pane.right = Display_settings.column_width
end
end
clear_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 end
clear_all_search_terms()
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
--? 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 cursor
if search_next_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) then
--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)
return
end
pane.cursor1 = old_cursor_in_cursor_pane
-- scan current column down from current pane
for current_pane_index=Cursor_pane.row+1,#Surface[Cursor_pane.col] do
local 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) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
local current_column_index = 1 + Cursor_pane.col%#Surface -- (i+1)%#Surface in the presence of 1-indexing
-- scan columns past current, looping around
while true do
for current_pane_index,pane in ipairs(Surface[current_column_index]) do
pane.cursor1 = {line=1, pos=1}
edit.fixup_cursor(pane)
pane.screen_top1 = {line=1, pos=1}
if search_next_in_pane(pane) then
Cursor_pane = {col=current_column_index, row=current_pane_index}
--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- loop update
current_column_index = 1 + current_column_index%#Surface -- i = (i+1)%#Surface in the presence of 1-indexing
-- termination check
if current_column_index == Cursor_pane.col then
break
end
end
-- scan current column until current pane
for current_pane_index=1,Cursor_pane.row-1 do
if search_next_in_pane(Surface[Cursor_pane.col][current_pane_index]) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- finally, scan the cursor pane until the cursor
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
local old_cursor = pane.cursor1
pane.cursor1 = {line=1, pos=1}
edit.fixup_cursor(pane)
pane.screen_top1 = {line=1, pos=1}
if search_next_in_pane(pane) then
if Text.lt1(pane.cursor1, old_cursor) then
return
end
end
-- nothing found
pane.cursor1 = old_cursor_in_cursor_pane
end
-- returns whether it found an occurrence
function search_next_in_pane(pane)
pane.search_term = Display_settings.search_term
pane.search_text = Display_settings.search_text
pane.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 do
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
end
if Text.search_next(pane) then
if Text.le1(pane.search_backup.cursor, pane.cursor1) then
-- select this occurrence
return true
end
-- Otherwise cursor wrapped around. Skip this pane.
end
-- Clean up this pane before moving on to the next one.
pane.search_term = nil
pane.search_text = nil
pane.cursor1.line = pane.search_backup.cursor.line
pane.cursor1.pos = pane.search_backup.cursor.pos
pane.screen_top1.line = pane.search_backup.screen_top.line
pane.screen_top1.pos = pane.search_backup.screen_top.pos
pane.search_backup = nil
end
-- Scan all panes, while delegating as much work as possible to lines.love search.
function search_previous()
if Cursor_pane.col < 1 then return end
clear_all_search_terms()
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
--? 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 cursor
if search_previous_in_pane(Surface[Cursor_pane.col][Cursor_pane.row]) then
--? print('found in same pane', pane.cursor1.line, pane.cursor1.pos)
return
end
pane.cursor1 = old_cursor_in_cursor_pane
-- scan current column down from current pane
for current_pane_index=Cursor_pane.row-1,1,-1 do
local pane = Surface[Cursor_pane.col][current_pane_index]
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
local current_column_index = 1 + (Cursor_pane.col-2)%#Surface -- (i-1)%#Surface in the presence of 1-indexing
-- scan columns past current, looping around
while true do
for current_pane_index = #Surface[current_column_index],1,-1 do
local pane = Surface[current_column_index][current_pane_index]
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
Cursor_pane = {col=current_column_index, row=current_pane_index}
--? print('found', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- loop update
current_column_index = 1 + (current_column_index-2)%#Surface -- i = (i-1)%#Surface in the presence of 1-indexing
-- termination check
if current_column_index == Cursor_pane.col then
break
end
end
-- scan current column from bottom current pane
for 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]) then
Cursor_pane.row = current_pane_index
--? print('found in same column', Cursor_pane.col, Cursor_pane.row, pane.cursor1.line, pane.cursor1.pos)
return
end
end
-- finally, scan the cursor pane from bottom until cursor
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
local old_cursor = pane.cursor1
pane.cursor1 = edit.final_cursor(pane)
if search_previous_in_pane(pane) then
if Text.lt1(old_cursor, pane.cursor1) then
return
end
end
-- nothing found
pane.cursor1 = old_cursor_in_cursor_pane
end
-- returns whether it found an occurrence
function search_previous_in_pane(pane)
pane.search_term = Display_settings.search_term
pane.search_text = Display_settings.search_text
pane.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 do
if pane.line_cache[i] == nil then
pane.line_cache[i] = {}
end
end
if Text.search_previous(pane) then
if Text.lt1(pane.cursor1, pane.search_backup.cursor) then
-- select this occurrence
return true
end
-- Otherwise cursor wrapped around. Skip this pane.
end
-- Clean up this pane before moving on to the previous one.
pane.search_term = nil
pane.search_text = nil
pane.cursor1.line = pane.search_backup.cursor.line
pane.cursor1.pos = pane.search_backup.cursor.pos
pane.screen_top1.line = pane.search_backup.screen_top.line
pane.screen_top1.pos = pane.search_backup.screen_top.pos
pane.search_backup = nil
end
function bring_cursor_of_cursor_pane_in_view(dir)
if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
return
end
--? 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 bottom
if vertically_ok and horizontally_ok then
return
end
if dir == 'up' then
if not vertically_ok then
Display_settings.x = left_edge_sx - Margin_left - Padding_horizontal
end
if not horizontally_ok then
Display_settings.y = cursor_sy - 3*Line_height
end
else
assert(dir == 'down')
if not vertically_ok then
Display_settings.x = left_edge_sx + Display_settings.column_width + Margin_right + Padding_horizontal - App.screen.width
end
if 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 wrong
assert(App.screen.height - (cursor_sy-Display_settings.y) > 1.5*Line_height)
end
end
--? 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)
end
function clear_all_search_terms()
for col,column in ipairs(Surface) do
for row,pane in ipairs(column) do
pane.search_term = nil
pane.search_text = nil
end
end
end
function keychord_pressed_in_maximize_mode(chord, key)
if Cursor_pane.col < 1 then
print('no current note to edit')
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane == nil then
print('no current note to edit')
return
end
if pane.editable then
if chord == 'C-e' then
command.exit_editing()
else
edit.keychord_pressed(pane, chord, key)
end
else
if chord == 'C-e' then
command.edit_note()
elseif chord == 'C-c' then
edit.keychord_pressed(pane, chord, key)
end
end
end
function keychord_pressed_on_editable_pane(pane, chord, key)
-- ignore if cursor is not visible on screen
if pane.cursor_x == nil then
assert(pane.cursor_y == nil)
panning_keychord_pressed(chord, key)
return
end
if chord == 'C-e' then
command.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 = nil
end
end
function 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 pane
pane.screen_top1 = old_top
bring_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 after
return
end
Editable_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 after
end
end
function keychord_pressed_in_normal_mode_with_immutable_pane(pane, chord, key)
-- return if no part of cursor pane is visible
local left_sx = left_edge_sx(Cursor_pane.col)
if not should_show_column(left_sx) then
panning_keychord_pressed(chord, key)
return
end
local up_sy = up_edge_sy(Cursor_pane.col, Cursor_pane.row)
if not should_show_pane(pane, up_sy) then
panning_keychord_pressed(chord, key)
return
end
if chord == 'C-e' then
command.edit_note()
elseif chord == 'C-c' then
edit.keychord_pressed(pane, chord, key)
else
panning_keychord_pressed(chord, key)
end
end
-- 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 = 0
if pane.title then
result = result + 5+Line_height+5
end
result = result + Margin_above
if loc.line == 1 and loc.pos == 1 then
return result
end
for 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)
end
if pane.lines[loc.line].mode == 'text' then
Text.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) do
if screen_line_starting_pos >= loc.pos then
break
end
result = result + Line_height
end
end
--? print(('viewport at %d'):format(result))
return result
end
function keychord_pressed_in_search_mode(chord, key)
if chord == 'escape' then
Display_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_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- don't forget search text
elseif chord == 'return' then
Display_settings.mode = 'normal'
clear_all_search_terms()
clean_up_panes()
-- forget old viewport
--? print('return; exiting search mode')
Display_settings.search_backup_x = nil
Display_settings.search_backup_y = nil
Display_settings.search_backup_cursor_pane = nil
-- don't forget search text
elseif chord == 'backspace' then
local 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 state
clear_selections()
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
--? print('backspace; search term is now', Display_settings.search_term)
elseif chord == 'C-v' then
Display_settings.search_term = Display_settings.search_term..App.getClipboardText()
Display_settings.search_text = nil
-- reset search state
clear_selections()
Display_settings.x = Display_settings.search_backup_x
Display_settings.y = Display_settings.search_backup_y
Cursor_pane = Display_settings.search_backup_cursor_pane
-- search again
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
--? print('paste; search term is now', Display_settings.search_term)
elseif chord == 'up' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
search_previous()
bring_cursor_of_cursor_pane_in_view('up')
Surface.cursor_on_screen_check = true
plan_draw()
end
end
elseif chord == 'down' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
pane.cursor1.pos = pane.cursor1.pos+1
search_next()
bring_cursor_of_cursor_pane_in_view('down')
Surface.cursor_on_screen_check = true
plan_draw()
end
end
-- things from normal mode we still want
elseif chord == 'C-c' then
if Cursor_pane.col >= 1 then
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane then
edit.keychord_pressed(pane, chord, key)
end
end
end
end
function keychord_pressed_in_search_all_mode(chord, key)
if chord == 'escape' then
Display_settings.mode = 'normal'
-- don't forget search text
Display_settings.search_all_state = nil
elseif chord == 'return' then
finalize_search_all_pane()
add_search_all_pane_to_right_of_cursor()
Display_settings.mode = 'searching_all'
plan_draw()
elseif chord == 'backspace' then
local 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' then
Display_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)
end
end
-- 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 = y
for 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 then
local line = pane.lines[i]
if line.mode ~= 'text' then
return {line=i, pos=1}, y_offset
else
local 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-indexing
y_offset = y_offset - nlines*pane.line_height
return {line=i, pos=pos}, y_offset
end
end
y_offset = y_offset - height
end
-- y is below the pane
return {line=#pane.lines+1, pos=1}, y_offset
end
function 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' then
return Line_height*#line_cache.screen_line_starting_pos
else
return Drawing.pixels(line.h, right-left) + Drawing_padding_height
end
end
function stop_editing_all()
local edit_count = 0
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
if pane.editable then
stop_editing(pane)
edit_count = edit_count+1
end
end
end
assert(edit_count <= 1)
end
function stop_editing(pane)
edit.quit(pane)
-- save symmetric links
for rel,target in pairs(Cache[pane.id].links) do
initialize_cache_if_necessary(target)
save_links(target)
end
if Display_settings.mode ~= 'maximize' then
refresh_panes(pane)
end
pane.editable = false
end
function panning_keychord_pressed(chord, key)
if chord == 'up' then
Display_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.y
if up_py > 2/3*App.screen.height then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
elseif chord == 'down' then
local visible_column_max_y = most(column_height, visible_columns())
if visible_column_max_y - Display_settings.y > App.screen.height/2 then
Display_settings.y = Display_settings.y + Pan_step
end
local down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)
local down_px = down_sx - Display_settings.y
if down_px < App.screen.height/3 then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
elseif chord == 'left' then
Display_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.x
if left_px > App.screen.width - Margin_right - Display_settings.column_width/2 then
Cursor_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)
end
elseif chord == 'right' then
if Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) then
Display_settings.x = Display_settings.x + Pan_step
end
local right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_width
local right_px = right_sx - Display_settings.x
if right_px < Margin_left + Display_settings.column_width/2 then
Cursor_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)
end
elseif chord == 'pageup' or chord == 'S-up' then
Display_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.y
if up_py > 2/3*App.screen.height then
Cursor_pane.row = math.min(#Surface[Cursor_pane.col], row(Cursor_pane.col, Display_settings.y + App.screen.height/3))
end
elseif 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*2
end
local down_sx = down_edge_sx(Cursor_pane.col, Cursor_pane.row)
local down_px = down_sx - Display_settings.y
if 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)
end
elseif chord == 'S-left' then
Display_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.x
if left_px > App.screen.width - Margin_right - Display_settings.column_width/2 then
Cursor_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)
end
elseif chord == 'S-right' then
if Display_settings.x < (#Surface-1) * (Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) then
Display_settings.x = Display_settings.x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
local right_sx = left_edge_sx(Cursor_pane.col) + Display_settings.column_width
local right_px = right_sx - Display_settings.x
if right_px < Margin_left + Display_settings.column_width/2 then
Cursor_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)
end
end
elseif chord == 'C-down' then
command.down_one_pane()
elseif chord == 'C-up' then
command.up_one_pane()
elseif chord == 'C-end' then
command.bottom_pane_of_column()
elseif chord == 'C-home' then
command.top_pane_of_column()
end
--? print('after', Cursor_pane.col, Cursor_pane.row)
end
function visible_columns()
local result = {}
local col = col(Display_settings.x)
local x = left_edge_sx(col) - Display_settings.x
while col <= #Surface do
x = x + Padding_horizontal
table.insert(result, col)
x = x + Margin_left + Display_settings.column_width + Margin_right + Padding_horizontal
if x > App.screen.width then
break
end
col = col+1
end
return result
end
function refresh_panes(pane)
--? print('refreshing')
Cache[pane.id].lines = pane.lines
for x,col in ipairs(Surface) do
for y,p in ipairs(col) do
if p.id == pane.id then
--? print(x,y)
p.lines = pane.lines
p._height = nil
Text.redraw_all(p)
end
end
end
plan_draw()
end
function clean_up_panes()
for x,col in ipairs(Surface) do
for y,p in ipairs(col) do
p._height = nil
Text.redraw_all(p)
end
end
plan_draw()
end
function App.keyreleased(key, scancode)
-- ignore events for some time after window in focus
if App.getTime() < Last_focus_time + 0.01 then
return
end
--? print('key release', key)
Cursor_time = 0 -- ensure cursor is visible immediately after it moves
if Cursor_pane.col < 1 then
return
end
local pane = Surface[Cursor_pane.col][Cursor_pane.row]
if pane and pane.editable then
edit.key_released(pane, key, scancode)
end
end
function clear_all_pane_heights()
Text_cache = {}
for _,column in ipairs(Surface) do
for _,pane in ipairs(column) do
pane._height = nil
end
end
end
-- convert x surface pixel coordinate into column index
function 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 col
function left_edge_sx(col)
return (col-1)*(Padding_horizontal + Margin_left + Display_settings.column_width + Margin_right) + Padding_horizontal + Margin_left
end
function row(col, y)
local sy = Padding_vertical
for i,pane in ipairs(Surface[col]) do
--? print('', i, y, sy, next_sy)
local next_sy = sy + Margin_above + height(pane) + Margin_below + Padding_vertical
if next_sy > y then
return i
end
sy = next_sy
end
return #Surface[col]
end
function up_edge_sy(col, row)
local result = Padding_vertical
for i=1,row-1 do
local pane = Surface[col][i]
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result
end
function down_edge_sx(col, row)
local result = Padding_vertical
for i=1,row do
local pane = Surface[col][i]
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result - Padding_vertical
end
function column_height(col)
local result = Padding_vertical
for pane_index, pane in ipairs(Surface[col]) do
result = result + Margin_above + height(pane) + Margin_below + Padding_vertical
end
return result
end
function most(f, arr)
local result = nil
for _,x in ipairs(arr) do
local curr = f(x)
if result == nil or result < curr then
result = curr
end
end
return result
end
if Current_app == 'run' then
if run.keychord_pressed then run.keychord_pressed(chord, key) end
elseif Current_app == 'source' then
if source.keychord_pressed then source.keychord_pressed(chord, key) end
else
assert(false, 'unknown app "'..Current_app..'"')
-- ignore events for some time after window in focus (mostly alt-tab)
--
if Current_app == 'run' then
if run.textinput then run.textinput(t) end
elseif Current_app == 'source' then
if source.textinput then source.textinput(t) end
else
assert(false, 'unknown app "'..Current_app..'"')
end
function App.keyreleased(chord, key)
-- ignore events for some time after window in focus (mostly alt-tab)
--
if Current_app == 'run' then
if run.key_released then run.key_released(chord, key) end
elseif Current_app == 'source' then
if source.key_released then source.key_released(chord, key) end
else
assert(false, 'unknown app "'..Current_app..'"')
end
end
function App.mousepressed(x,y, mouse_button)
--? print('mouse press', x,y)
if Current_app == 'run' then
if run.mouse_pressed then run.mouse_pressed(x,y, mouse_button) end
elseif Current_app == 'source' then
if source.mouse_pressed then source.mouse_pressed(x,y, mouse_button) end
else
assert(false, 'unknown app "'..Current_app..'"')
end
function App.mousereleased(x,y, mouse_button)
if Current_app == 'run' then
if run.mouse_released then run.mouse_released(x,y, mouse_button) end
elseif Current_app == 'source' then
if source.mouse_released then source.mouse_released(x,y, mouse_button) end
else
assert(false, 'unknown app "'..Current_app..'"')
function num_panes()
local result = 0
for _,column in ipairs(Surface) do
result = result+#column
end
return result
end
Cursor_pane.col = math.min(Cursor_pane.col, #Surface)
if Cursor_pane.col >= 1 then
Cursor_pane.row = math.min(Cursor_pane.row, #Surface[Cursor_pane.col])
end
plan_draw()
end
end
function load_settings()
local settings = json.decode(love.filesystem.read(Settings_file))
-- maximize window to determine maximum allowable dimensions
love.window.setMode(0, 0) -- maximize
App.screen.width, App.screen.height, App.screen.flags = love.window.getMode()
--? print('max height', App.screen.height)
-- set up desired window dimensions
App.screen.flags.resizable = true
App.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.height
love.window.setMode(App.screen.width, App.screen.height, App.screen.flags)
love.window.setPosition(settings.x, settings.y, settings.displayindex)
Font_height = settings.font_height
Line_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_width
for _,column_name in ipairs(settings.columns) do
create_column(column_name)
end
Cursor_pane.col = settings.cursor_col
Cursor_pane.row = settings.cursor_row
Display_settings.x = settings.surface_x
Display_settings.y = settings.surface_y
end
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 column
command.recently_modified()
end
-- for hysteresis in a few places
Last_focus_time = App.getTime() -- https://love2d.org/forums/viewtopic.php?p=249700
Last_resize_time = App.getTime()
function App.initialize(arg)
if Current_app == 'run' then
run.initialize(arg)
elseif Current_app == 'source' then
source.initialize(arg)
else
assert(false, 'unknown app "'..Current_app..'"')
end
function initialize_window_geometry()
App.screen.width = App.screen.width-100
* `ctrl+e` to modify the sources
## Modifying the app
Hit `ctrl+u` from within the to modify its code. The infrastructure works, but
it isn't advertized within the app because this particular app is currently
too large to comfortably modify from within itself. I use more specialized
editors 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.