diff --git a/README.md b/README.md index 28bcb22..64b5efe 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ npm install -g ./cli albedo ``` -optional: `view_code`, which lets the model see code as highlighted images for -a final review pass. needs cargo. +optional: `view`, which lets the model see its changes and code as highlighted +images for a final review pass. needs cargo. ```sh native/render/install.sh # then enable `view` in /extensions diff --git a/docs/extensions.md b/docs/extensions.md index 88f65cb..efcf6a0 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -80,9 +80,9 @@ interfaces: [`PythonApi`](../priv/python/albedo_api.py), [`work`](../src/albedo/ ## view -optional. `await view_code(path, start_line=1, end_line=None, *, columns=79)` renders a range as syntax-highlighted PNG pages and returns them with the cell's result (see [images in tool results](#images-in-tool-results)). its instructions ask the model for a final review pass before it reports code work as done: view each changed region, check it against the surrounding style, for repetition an existing helper should carry, and for leftovers, fix, and view again. +optional. `await view_code(path, start_line=1, end_line=None, *, columns=79)` renders a range of one file as syntax-highlighted PNG pages, and `await view_diff(path=".", start_line=1, *, staged=False, columns=80)` renders `git diff` the same way, without line numbers since a diff's own lines are not the file's. both return the pages with the cell's result (see [images in tool results](#images-in-tool-results)). the instructions ask the model for a final review pass before it reports code work as done: `view_diff` for every change, `view_code` for context and untracked files, checked against the surrounding style, for repetition an existing helper should carry, and for leftovers; fix, then view again. -the whole file is highlighted, so a range starting inside a comment or string is colored correctly. lines wrap at `columns`, tabs expand to 4, wide characters take two cells. up to 4 pages of about 80 rows, split evenly at line boundaries; the returned text names each page's lines and the call that continues. +the whole file is highlighted, so a range starting inside a comment or string is colored correctly. lines wrap at `columns`, with an arrow in the gutter on each continuation row; tabs expand to 4, wide characters take two cells. up to 4 pages of at most 80 display rows, split evenly at line boundaries, so wrap-heavy code fits fewer source lines per page. the returned text names each page's lines and the call that continues. `view_diff` leaves out files git does not track and asks git for a repository first, so outside one it says so. rendering is `albedo-render`, a rust binary in `native/render` (arborium's tree-sitter grammars, bundled JetBrains Mono), run as a supervised `bash` job. `native/render/install.sh` builds it into `priv/bin`; `PATH` also works. a language without a compiled-in grammar renders unhighlighted. diff --git a/native/render/Cargo.toml b/native/render/Cargo.toml index 7c2e150..eb24e84 100644 --- a/native/render/Cargo.toml +++ b/native/render/Cargo.toml @@ -2,7 +2,7 @@ name = "albedo-render" version = "0.1.0" edition = "2024" -description = "Renders a highlighted range of a source file to PNG for albedo's view_code" +description = "Renders a range of a source file as syntax-highlighted PNG pages" license = "WTFPL" publish = false diff --git a/native/render/README.md b/native/render/README.md index 3fa72a7..97f1199 100644 --- a/native/render/README.md +++ b/native/render/README.md @@ -2,11 +2,11 @@ Turns part of a source file into pictures of the code, with syntax colors and line numbers, so the model can look at code the way a person does. albedo's -`view` extension runs it when the model calls `view_code`. +`view` extension runs it when the model calls `view_code` or `view_diff`. It colors the whole file, so a range that starts inside a comment or string -still looks right. Long lines wrap at 79 columns, and long ranges are split -into several images. +still looks right. Long lines wrap at 79 columns with an arrow marking the +wrap, and long ranges are split into several images. ![Lines 86-104 of src/layout.rs, rendered; line 97 wraps](example.png) diff --git a/native/render/example.png b/native/render/example.png index 4f76c3d0a53eebdc84dc52fbda11552e0b2d2ed0..e39af8b00373bc0153b43653631e851fe73c61bb 100644 GIT binary patch delta 26069 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 delta 25891 zcmccdl;!_pmI=yChQ~IlPCQ%x@W9Po*Zp%v%iFoj=UxrVzH0SKZtJ$ZwcFn9EqKq_ z!OhP1P3?`Z(7*cy5{J3iSy=zi|E|5hp#1#Zr+VKDe%|}ObKz?B{SypSRIa$b)VZ`v zuX4G@#)Gw&ehYur)XvGtN%>Wmy~%#H%-^5i#kv^2y}5O5U9R*ppA|tHWxthu>-ke( z)==r~%AKE?xbQ@&Y1WMivK|NB&)(^-W@2jA5X-!?;^O4xbqg0JzGq^dJNI)Fzq&iS z*Ojxg_g~mrXwQ_eykE|KVb~S6udjW7CknYb71zz%pLkcR{#V*EpShM57Z^_cTCE}b^R9V}-J(so zi~U1yhjI%qf1UB^=8FkmG82F1WtS`8-Baw!v^Z$3Z^?}p6HfBV7qc(ZWOBK5!fyTS zxt>Xy`ac2|<`naF_^i!aAuBfBPyf}`%YqxU53zM@^o&&dH7lpY;|9Z&#r6Mh`->_Y z8&5X7dHrwwZzF~P#w)@KQqq#O#x*Cj|6gCZGP5MaoiSK?q%4xX~L-`3lks4Rz2Ch#&OzbMU#a#y-CelRuP{rbDG%k zJpAlcc#o--Y2I`Fb947kh~m@~4rntz`?JRGwqxk)x%=lo5~|l&8FJ`xeS}b>gWW8X z@4J$+T9&4WnuZ1CSUq_CZ|j`78r3d0HCN4ibE5eUyG_lP9~UMr{vWhT^E=bSk8;V& z{O6vTBE9vj>M@Uh^MuyfOxSmT-&U&U*zaOvuK2S@`A(YG1!l0ko8C0nqH>m*#?m7i zbHh&Gto*Y?e$E^jO=oBJ>iYXU@?K|mXn9%Ixuo89o4;z?YPYl7nEth|ReSJxjj#Qj zOs`BEJO7;__jE(8{x<9jIT~qevo(%A;+f^;N32Jen9eJTtXz;ZvDn}Fm%*IyS&{Pg zO7~w(QTzXKd1dl3->QP=ZS&$Z^ZJ+>VphD}$8e?K&Wgn5HEY(~t^Q|fY8oh0Uw=8S zL25YMN!I6RTFn>{z|G|xzxx&EE5bl(yyHcH7v$fX~_a@QD)|NB`G{glzFqGdB9g@;#~b(Ttfh-CSHW z&VB6f=-5;FdfTU^bG2T)eyOUWrj@rxTt?{B6gOY@_R`hiG5ZRRtt~y>+C6>i>l;V! z|2GZJ+u~+;(Xgc8&^NXx^ER#AF;#Er!MHs&lG55`6Sn(V+1A&6GEp;|_DEpcmrGAq zv-8O=oG;H_{afFB5&e>$aI)o@ty;LP)d>$?hH-1heK3}gvXT~uJBX&G5~ZqXwtBeTVecWs%Y zRVBUh#~uIB+cO_!_pNM9tM_VCF|x})S02}qT~q$-!rDDQqa`w3kEAb2dlK9GQSEz) zE%)M}J9j3U&2(Ay?6Rm2kBRZ7l!t|t?!WVugKjqF=<61HQ}hkU3LqvJypt8$K$F}_k39%w=KHnfktrN-(TMro~V1( z6Uo14&-$3%|Ndt5d)l9QAi=UF_qAK$;V+TJdG~kzzW=W|{amHdQg{2$tHX45SAD!A zE_=KC{{7=iY%+R{HgB>~^ZB@^pzwYD@9XtzZa;XD;P08Y?aIlMFE1`uH)*+)aPFMx z%LB&_9B}y9Ag`Z4|EB%f#ql5A`{&=;{XgOJwcL_9!3w5Zi@(oXdOE!C+l+`Ov5VKv zdE$8?xc=BZ>zNh*Lmo}et~}`8%(p92((?HAV=VUPE8b4tmS1>f;#I??riQ`UqdYpSYm_d2p0vok=A>7Z_>9>zd46?i%6BnO z(~bM_`;_T(8~p(NrMc&W>PBzu>TT@l>6!L#+ktcE!YcoLD=Yir_pvK4&~#@Rcc68-^ZVkH>-RoADQYa8 zc)0IVyl;D()!t*XW9|w6f3(gfJ}FIG#_0Ty{Q9 z-Za~(IX}KDx!d*dD>m2VYptH?Pdv5XuuK2#*{7@joS(X2ak>2*5$odP!ByK$uE||J zy6;GR@FLqV==Zt>sCs0 zjh;wcx_tR)VWCswgea9t*Vt|WSz+hRx`&PLqy+t3s^z51Yx{VU%)-DUW?644qSilj zX8*sfz|Pvh$SCW;gT#dk6K$(Y7OVXh%5M#3*gEslsY?f=C#H+7($ihub=z(4Z(a^g z&fME>b4T;>d3|XH)%Efu^&d*XzS|GIeiErcOdgdL*XO4y# zeObLYV~Wn&JKUL}vA#iV2bzvP&X$Ot@|S&I-QHc|=jPtpTJ0ZE(UPQaw7^tW_SyNr zo`rMsR|aedI(%wX&eEqLg||DJ*kp5ayX+)?3P*C?o4K?pt2!x3=y&J3v$I9RokOn3 zWW7uiu6AAY{?sa|>&$QZ-3~mrj1sBeeP+tDkie`p%RTj%t@-u!?c$|NU0=T4v7=(< z8)a2%hJW*aR7f5%a$pl=U|@LgIQVnu83qOhhJdSz4lE1|3=K5`3QPh5ey>&U1mgQ*Ng9Sr~bPo!+>oo%)~@AuJHTcssdSS2MLedp{}^8V`e?-!GoebU?% zSKE^M?YmcXnR4q&hE@NA_sQGl9e-!*Gc%@sU(weiD=+(~2G28H)lwI`<;m@d$v39o z|193>z9J|3;rab$+G0OmeUXbuW#%sAN>gO{c+)Ypr4=L=4HMT zrK@*Xt}c5S_+{E>#xED^`?8brmK+T+V$GU6ckNmE*J1kcJF6~S5Zqm!lYMtpQEGkn zn((a1XX53-@pt`CpPFEQVd1*jE9$CkT?3;geK~L`|E-NRB5ogC_bY{e?yYmIZXNyo@}^<`Sc=otm+#Umonf>x zq*~>a@PCW+xMsGm>b@Tmw1lTyZT)|1$2=sra|sT^CwU&j_me_vh$O?&&l9bxlm({5k4<<;Iba z!Y`?p^6!=3=lNfJ_?Fey?5SC|T)U@VbhHd+)XF<|bfQ;r-Obn6voF5o-WRC;&b@v~ z>eQuP;aixt_VMlc{pDTdn-h%2!J9LU>(87xk)YN4=vC;?=|6wYy>eJP=}=sH+LBW* zR8OA!en)>IUzXzvUGB{Zmm=w1imRhOi>sM)8}_2%MAij-~LMX-$6v?SXT5yQ@$7Bwxe(d3Wy|c(&H8qDOXR%g`rlsPZeJ-A za((qDozK?yOXTKDZePb1AMqu!=j|QsCwDe&`n!CIZJk!H|AyFo7d8aDKi;=D?yHQ0 z`;}$R>|zpYx19X#)yK#DQAM1XsdY@30 z`eXme3gH7~m$g4?T`$(Du$gz>`M%v>Hj|uNMK3zuGcz$y^2Z^)NFN@_}PCt9{)G4hEA=B5#{5@6c(R^&(y0y1_|0TM)xGY(8;`_zqUkX+Z zpCs&e7Jj}KU-GW-%Z<)G`I@F%?_Y1ew&u69VNqJzwQ%P4Kl_qQgFk94_P?Lf{_U;E z|K;_kw&mTwxzt@gD&d!rd#|j&d-i<4F8@+9IT==@UV4vu1`OT;2 z?98~c=jx=Tr@c)(x2gUAqMx^)cfI<9)&Es;vUEvsG5+C5E0z23rk-JGp` zSGN|cH#!vDKRx@Q&KdjS+h+>u*KcXw@4o(*+K=;{^$Y#iJ+;o)f0BA^-p`5J?dD6?l>B&> zD7ALtqtFFku03p#-K^a7$7q`mcf6ra#rr0Y!isI5IQC7SWZuUg`#1MP=U2l^ezSw++i~s(y?{Ou!AI&y4-Dho_^0c{p;>uG?zsDc_8dY(-to}*l^pE1p z^6MwfnRmAK+^bvmSpoARFDg$ySyWV1v1iZ!S7PhV-rBJ>ZMU(J@yqWA4jzn)kMD0h z@3iZ`bQfRorp;@gom&)>eCOh1#>dMyy4pxo4+|ROx8^+D_56Nq@^SwlACI5)Pm3oD zKN6m2`+2cI?^|B}{)%5``jfWS)&4h6+3=fLU;nA){oVf)zMbFSQBrcOPtNnLh;{k< z&^VR+`R$8s)woy>MH(-=v?Y9f{QtZw5vxArWO{jV)m}JtH9Pyt1}W=b)x5lE$NKF9 z{pNPnmz0*CJ$tt7-7oix*UFyWGL2f<#{0@x=FCj%{hisbne@3fOXLX&32olADR)h` zMQmHy9q(ytSA<`G`rLA}TA%dqKSxh5yx+6r!PG7nCBdzSE_iE;-}cq7t^He6C#`yJ z=c6<7G0VLtzw%*SU}afyr2OX1oBPkqHLkhwK-s-t`})cHPgks1yP|x2R=K*n8_x+m zApX(p^{aao&c=?f_pxn>x7(;__9#{@T1s}#*7Q0N*Sc6YDf8EPAD(1!9i5mnahsmM z!j%u-e8O@rG^oE^xZdb&uDuJh)~nE&(=AmUtDdcosA}DFY5{BV?w4O*IY!-U>a19H z&8aGN+3E1L2cC3pyj0(@edhIVN6lZXxV>d{+=lpL2U9OvZrT&dQ8lgbm;c6P+uUB) zwK4hjvZr&I2^K{zR?572a|X{Zt)jcjdbTr8VY!%lCm{Ic8}nZ$ld~(bytMuXi_F+& zQ|es0_V>Tt`Es^(PX8ZV@nyWG&n=e8)f#J6dvZl*THd<}XO*>rKkh%jqW(_x>n@k@ zb#cqJzJH57u)Cq*wRzsjl$KuJGrR7fr*M^3-S4G$ zpB}$B_u1V5mqwF4pY_e}yDh7`esW)}?dARF_eyVgWA}aL>gAm`j~)B<_I7#U$B$Qg zp58ux`}p*GYqPIt*gWeN*H;ztJp9W;zOLTx$bZ=+6BC*3e-EDjoB6!6Z@-QPgt8BanasvYLQKP$1BCZ>ettYPv4c9z9scG-z|Ad>#Ixd8K>L6tUdqk?J_mB zD<3cKiLam9ul({r;i)B@a>;H%9fzN^ZOFc!zl1ray4w1vMXXi%EX%*_uU>pfJ)UR# zJ1DH@ufnaDF*_n$m3et_2@70H*ERC3)epYXX!o)9 zPs_JY+2OkB`Rl)89_5J+0v%Agw zdF$WK&3mblKmEJqe4qQrAMCuUDKq7KjQ8Q<=$Md%u+r+v(3RKSrdJ(~OO3idasG*~ zpW>wA4_wL93_26$x>xISy^;5(l!ND=e$38lGI2k);qH+$YVU(H5^p@U+#M++J};(% zZ%Wl0t4&h7o1VP;xa7u$oU)?*w?8dDlDI$Wh1AED&5zFQ_)zmRq%S)p;?vyUk%1RC zZTZr`c{yA5_jG+B&FN;@w_5fJy`CN~F1j=Ibc}h(b*o?QzY{;TT+KEQxKR9}{=~&E zUwT5dr+xZaak7&$dJBPCKrYycB)f`r2mG1##i&j~%t;Dt@`}=4jg=x>;nW5UsU)&yHAL z;YzDJ*J8F5-0xoQ*vb2bXXlw1TgIgJZ@K*SvUcCz*R!wD(Y<{6a`Mq6FJ=pi8{%@G zUk2MVGuK*M&uRGa^8^p4=&V^O?rxX6tA6g<^<|!@kQ<}YV!kiykCs_jp7ht~>pq=* zkBgIY>y`}xF$=G)NOUe!_&)iR==Jqi*QxHmVes*>zj+caXzdCMI{RyYVQ4MU3YeNV1j$K^U+V!SJ>QltF+i{62c-grEYsr{8UE@j7DvupQPDEe()!LK9c%mZKhsrD*M<{QwqBb$zy6Wc{j9K0DgJI=ad9e& zL5s3a`1eg#dHyds^VNyNM_M#Er4@GV=(O2>eD99gZj6$u4>j7uH+S+$dxgm;7eD-A z|Kw-uWa%W$dC9xIPam?&uRUAzdG5^j`p%x-lU}{}eB(uc-7K4qki)%2LGt#szu#H} z{@o>J-_w6@oAs0Dt>0ePPg}mfO4g*buk&|!+I{}VMw2&P?G?(tThVVQt{*$o^!M`( zA3ns_|GateU}K=RmwXmiw;lV&9sg@ZtKZMsDc;Az!WOva#*Vi&OMSiXo&Snx=$ z_vy{}!oaQDoMu?Wx=%ju*1p7_`*eBqZt0iLa{CUf2+0Q&+Od;%xSpF61&rzM_aeYNTj3uiCC^N*k2wKX;Fl|CoV zoY}Hz%YvCAlJ7M5cwa8~Hh2I0MTai42i8YhKE2`Obokw!z3#g$j~Hajc{A*jv&%e78yLwtxpXaHbRq+`S71QN@>K&c%wB|>=M)6v0gEYx~&POfnuVmT0 z?~wgmSI=e?>6g^=T z=dvySSXawkm&3RCi|O;%VRHMAm})8fdlLDpEBoJPbJHam*H=ke6sD#49^av?-ZD*W znKE19Oy`-e-8#4Sl?e(TudeW!;p4mIK9BsV6|>XUDRp<%|Gpo-@%y!l7ZH z6&|?$H`~%WP_ECYBX)O5lDppdf2&+V^R!PCNihC*78U+U7U+u6=*n>Kmz>7U6ytO56Y z#c!lu{-5;ETTp1An9T*Xfd%d1QNOSR3Ge}2Zg^jS`0BqG1!S_Nu9$Z)j=olEK_-{6Rb?<1+ne7u_n{lqykuh6$Nnpj#&eYcLEvvF))6%ZZ zDwTfo_6<{W)3*0FY|GAg)Vr-c)+>87)qdHZ{p+jV`>s9N=y*{7@89gJi}oa*4gdG= z{XNzzRr4Kn&j0i3)jA&*P^jbF-g;_jj`{cbj;7m1Dk@J$&$_ieH~7bN3tio}XK$DL znaaBUJb$KPm6P)Kh=6G`BNKL9IAdb16!h(%%)>PuE1pD`FFpA)I(Nk^uN}|6)pIS- zvAr!Qs_K7NTQAAL*toZ|Z(-HZZhw2OIS$Ay~>YUb}uadCUaBhC1l=1 zPnEN7Kf{^@2NqbBW^Mi`z!|-9DcXyxcEqUw0SO1vTH^zT|Zuih!xoutS);$MK z-<`KYe( z=^wL@Ge0jcs@j_Ry6=(RO1*VcR>ixxv^=W$+0k)g_Vzq=_2R69A+fJT{``$sU%upu z)~ksbtETU?sB-(WB%nKmNaAckpq+iLZ7ACasO$TekQA zYxC@I{pELVuiE-r<6hF0E}@FNwb4_vgsxWStE(Ru*N;DTtV{NZ?g_vC`8K_sy-On| z>OS4He-+34GHsFX^OLd`%$%gy*>gU6m9_p6(|y*(FT(C^s*i-twuF8%-?{vDltf|e zl~$|%9HGgR%?}Hmg(eG`=ic)=SvlRR^5o09+TSu(MQ4%^O-XcSzqqc2@0^V6 z8&UR6w!2F2?#g{WCDYjUSRWUE|Ba2==1bR2T|XyaVQ%#H=JtNS`KIYNCOA4CR7^it zsWtuD`uO+9&PH!(FIpKqd(xxhesfa1GEXdMOg`qjj z?Cz3nC+{{r{cqMa+t@vLxzC#LQ?kw@R=6$IErzj#s~!%IcO>@2=b7 z{r245>acZj?zdkYyer+cxz9@{_4SkbE0UY{{V#Lz%6io`HCst(@w+=a&Ll4MT2abX zpL^5oz3p!CLbFw`QuX|r`F8cMUm_DPeR(kQ@N_-o;R)>pjKeK-CO-`_d=q^)bphXswQ=VD6l2E@M3(A}1I|L0xv!?R8Dp1gQb z@buN!Oyjiq7F7WnA?N3EzuaGU>dMS7yZ2`w7kd}H@?N!dy``nBMMXF|0y4|Vpzb5nTi2sGZ-X3`RJ^f4ZxiiyrEq@rP#@^bN7q`N8 zN%Prv+6P0dw#{}faXQvitDhD5(zN{16ph3y(hB$XRL*ufP?md_OFYo%+f8HpTJbh!GOxhb+grB88QCWt-R0u{GuVGiwxwm>|H-*yx+6aZt9HMr zPrK8idF3`}=BFfJ&ZkRTJRd(jEvEms_UVa>kDu)Im0lmechbXU{_E$(#nszY|G!=T z?at3zTj#Lz{+VUx8n~dB^~-1YH0 zpUrvB`fBN%J^H*w+H zuKTNM|Jut=zm?zC+jncn(WFi6uKYP`6aH>{QeI_lZvI$k){1q~T3NAKSz4?nrfYQ4 z&rY}>dEvr=gpY@Q$_cBjzrCUGT2E$=Rei_fM=Q>TC{3O6$|U$|b-q=GkC)%O3;Sf1 z^Yhkup8admloX}+d-bD`yK4jP)O(%2b+ybV)2#Zs@%N{neqXl;5tp5P`_!hWxV{gk z+n&|?W*GgCx8HW3>Kekj?}`hGU& zl@31P`o)WrR}^mh@KG!@LFO9_$E|2AEg8nqt(m#V274e3L@!gDvaDxmvD- z1>G`cJ^eX1B^^hm8qPJJM7G7sr z5tk+kp79c4dL=nsH9H_>@2VH6di`HSGQ-^Z8aemxnBjJ`o@-^~?tQ-xtk+b~Fo>Bp zQL(dIeAeuhfA=^qTVfkjnI#+iN2+k*q)A@K-==zauDVyfdRbNd)!p|SCQP`nE%%9e z!n}H;%$nSc=ndcbWKH*I?)Mxf-+NxE70bJyO$|n7Hnd z*Xhp}&K!2{3+(oses1oJ{&mRHSuTx>>hE;3pPunxeNMkXDEHOf6{1V0a0FJbU7WmR zRv+Kz=l_=Mn6`O~_fo0;^@%xw-^`9Hy$*HNFRpy?;$vw@oblpE_N^1=zrT3p;JS{_ zp%u4o%@X~#e?e7$$h@fIBKzH9Uc4+kHBo$7)%&%xUi}T-yGzgN)~z?O+c$0Us=XW_ zs+FOwQm>}A(fjqSJ&}d~1-1SKSJ?1Ye@@!YRC_+`s@M031$#IrI} z%HMvv{AMN(1B1iT76w7k{MM(HH>4jigC@YHHbX@m8#q8RKNT=0$h4;)IdX(O?%vd& zmx>q|7=FA_bV#W`SK<0*zu=(|KEsVk-xZa2eSh`(_X|UiksmZ`{xLrK8(#Q5uB%&H zR6X@ueAWBeTkrKV3(fR@`04SB#{KuXJ5E(6?14-GEnl{bPs-}iyS?l@3=9kh+`s>1 z@3_3(e&(*Xo`p-**rK@GcAR z%#}B{%(PESNihKVL0){;jG6QAzSjA@ZQrsDLbalhnE^3L$)}b7l)!E*`LLk#4GFT|J#P2d3A4(fgUs%>kP#NSkJEG@dsR#tib!@l1^ zKF0N5^e#H6ytsU*=;4K3Y)T80o>}X$uCsYGf5x=CdoxSJpJ%zQi@JVpE(_DaTK)HR zwZBC)b`^fUc16zK^GI5v3pi}0`|QY_ zYi+wety#EEtp3$Db$=#d>075icem%YU;KSD{`M^ClauR(|9#T^bh*BB*|M~l|F^#oAtUs-#Ky<^G5+PK4cw$YVerp=gjc7^B7XEFjaqwWS+>*&pm zyRv$@^8ZQ9i-T@57@BFGc8Qu|F`c1>`@t?k&d_$raPQ>I_RcY^5^?N-Lk8L;aFZS?}^1TgOKx3qT+Z@tV zy1(x0yeqrYigu;kliMFqSpVy`?%kW0{Cdxn$MQ+-Jecld{=0nQO80jyqCW+pEzol9 z7!|SOz4N~~zYf#2F8|d8@_E4a^Y#ugCTsEnN^Y%6{wK6@kKy)dQ{|);{uQgfcyYJj zXohid{;Z75HD^^Faw2!%de-Ut>`mRZ@{h5vazl6DdY37`ZqmO^3+sO_``G*Z-N)qT z@#n+-Kd~|Y79{%U-nL_FpKpvl#~zRoasS$b-+pEL|NM@rd}cH==db+iK3RLqlnuL! z10|1y|NHml-k!){P|97vl(E8s{oUkhae)^v63&N&T`7{4nY?bjvVpPAqr9s@EB^$E zP87R;C}MX(W{p>vSKEyRK@Rn3{6??@qpM-z}zp zdv9`f`?p+Bd@z(AcGXH%fACpGx1?M9>1WfBDN?U9FRlLYp{J(@QdwNt>b?BYvf7`g z<{vt>%vZ`7l#)al7?YkR<>$MvbiDfb%KFzy#-&~R?DPuQ7#JG38rAC4K#ER0|GfV! zFNigT3%YQDolyzIe}bi0Z}@**X!4|Yck2&??17c&#{?M|xHtqDAN<#x>9eiy^0RBX z`y=+2F$aG{S&YEOu%IY7x)H9`YacvSp6D^f`|+d4qD$}D@h=V1 zWd5&zcUSH4Jr$hgk+wa%2vIMQ1HtpKl z+vVryS)V`f-+ST0#OL~Pi`;CNHQCnxd-SK(`^@rh7q5wXb+2NVU0?V8><4@EHEV-+ ziv=@s<2#8#%?y#rwci*&S&ps?{wyM3kv3$MU z7dbh>kH5Z3Z~JmGc>kI;I^P-NFMg=sS6gM8d+Ej7-3>o}u&^;a5We~K{JgBIJ?ZCX zovVp~R5)(zSLZ#~H)3FzI(3&($3(uqZ&p@TIqSXW-jb4%5}X*AlV4S}YuU01-;UfJDRbeMn592{{NR)P7khY_?(Hq_w&5x?x$o=~KKx~mv;F-g|HhK~ zqaX6!Sy|Y${Hq^4c*r+*muc=pv)XGiRv`&?Vsib@KVR7Hzn^i-4QKb7_=J^mzwCZJ z`+afU+S`{m>Hf*h^zvG@DC2HhZ~Sil&@XX&>lT;qXVj1Xxq0u?nER*0*VR3>TjH1; zem(51+J6IOC8eOiC%<19)?Qe8I(%j9Ch$yg{ky_1H#oO-+*18rYnyR)m%WH`dRkho zX~Bv2+F`~rG56r7dL&H_7>|rt6B7uV!NLoPG!8BkoNq7ii!NI zzU~vt-fdB-6n30ZuU7iIda3Lm+p4SP2kZTKYKwGTe=c5`@qA84z3AblYc=vkC8a(= zZ`vpO-#E`(J9T4j*pE{W4^}^`{}H2?aj0J0ZJn&-rox&RKi@t&Q!;bWiX%%GedApv zEqiNw@SCkuLQ8Vr-`Y9ZL1D-Hr?ER;96K9rqMLiO=Zp^g((4 z&9l}#+H`Ho6p>#GrmntzZAGGK*_ZE9QfGSP>#Lr9pVNgjpQ=}hm`_!4^H#cF%Mo^I zQmSD7=1uqALpnEJ>exQB9%J&+=x0!7R`#B6e#=#Rcu$M9r|$piyS&j*@utYhPM#lE z=FU``fA#9#k8Np9*J^oR)c-rucB7-b@}TdD?jGjTiz=C(btfJ(d4An4^LJf$&>H>U zJEwm?ay?Ts>rSNpm(XR8ioHc-=X^b{ANXR==GxjQv#x<=3|~I4KfO&h|30e)wkbpIl$el= z``dg=i$60l>lPVbhzVfXQ1EXL+t=5D+hG%v5!aXU?RL8~{6{3L)2)G_zUD zt~q&z`4!DEvC=rbt1s-tX`iV3I|?c%etNPss%xKw*8ltEd3v{OOy?wdm4!{Je>3OX zySBBzC9|?l_q)DaFtKQY$kouzB6UCRyk8x8ZA$-|wWoJ~i+2hM*w)&d*2(R&(s!oG z^Vvtm*wjARb}roc;FsMjv(>xirgeHBeUj%`b^VIYqh>q5+~xhtB({7gSS8KI_G_Ej z7XP&eL9zC#^ODt;{OT7UHrD^QPDsu4ar{c0JBxg0QQb3x-+QJxX8~gLXgn*;|YKhGHYTqjw?oGZoe};Zvx3QU7 zQoTOs@6D$I&&%C?ade9F-d%b*?UN@f?>&C>=*GQUK1>r6%PH}YGR--`_oQy4Q~k|r z*VmS=cH@-}kTWzj?X6n8IQfOGs_E7fsoS=1PJd#4{`BnP_OjfU|6dKDZKYQxTwR@C}L+6PuW|(*~H+UJtMmJquuhXh7-bX$$1kT96^<6R6 zRQu<7j5f8sBnmouFl2$`*#e!Z^t z=c})4-mDQk+`eJ??|{d5k9vo{VAwfrvoLdLV~83rFZcUPi~B!0M4oG(AKmp;bawQ` zgOldx)Gzp$$T2^>YqQjg1m)EM(_F(<9?hr=i_@~}Kd;AgTYIWk#UFleC4m#(nWmC? zr}pf#v)i}WMlB^REh;kVf{)0-`9^m0&zEInX72m^Jy^#5B70!8WvYxbWJc@fMaKBA z$yZ#PFILq1b$n`^9nkH*c1z*INiP<>af^Qy7$Z?HtC1hS%q{WhO3~+&Ia;JX_uVjj z|6Jgc(c=S`GEO;4&EU$|_OjL4Wa;^{vpKil_6YtJV7Rd6)}8lj96py6dM}MQuJk_g zZ(){oZ>Mq!!-_LkuAN)GcCGJ9!7Rtyd!NS#2MZq+G@D#~jqhkv*W`oQ*H^hRDp^lx zT-8wbdCr`yxKH&qXQxE3lvDorcuwW=b8~bJKJ2&MeJ5)B6QiA<(nWv7L@hqFRLV=x zMQ~~AFH3#oi6Ygm73KdMor|BX+Ql5-_~UTxf$OjT-za(VVk3K;-4@pu_I;ZCdDl@Zi#;S;o=)AD}I)JNMs-+r{Bu=0^wy|=7wCCm2@NuOr=KB`H6u2DX1 zQQirA*UG4E(aukQc$Jhstvq<-$dN;x~Ij=ooRZLN3u z)A-6?o`2A1fEoyD| zwj4Rq)5Bxcvfy6*gG+aee$5ti-v4Kjb7SMVbEcB#R-{fSx_U)WR5f4k%6Z$>Jtt!q z@BaR7`O>8upEI$sux(_%8T2^o*pw&TU0v(qV(Zva5&{hNZhn4!n#b2)Y^-csb6%cu zYgdVRDU_zCqpNFGmQnxa$jS>BHpr*@Y1=TZy?C$wdvNuy>G35Ya_j1o51h808n7td z_n*E$qx{~@;WMXnMAa>3+8iXbR4vSjON?1=`EoT6&4?#Py0sHxZF9}6R8CDd&_3dx zDjh!kyl$k<`3G5@7EfnngG{kP2*-IyaJ9a&+AYGcpg!ly&lP)*ELqaNum8>4oZQ^pDedMN0ro~-b(i&j zN!ERQ^z`NC=NaF_9m|)sc3oJXFW>ch`MDWi51jseYU}I!{oiJMIm{#1AI!8WH`;t< zh*j;iIk(H-{oDTk!OY@$Jn8Gs&i?xT`+g>UuF`STAgwJv`jxvS=BMt|qVM>U;R)31H9`0--a zjO%upcJn;H`>RG;WN57u39LVR#ZBVb{M0PfCm&5U9C+#`u3URMnfa_x)(1CY!r&H%ru)AMw8Zerk@3~e#b{|yI6hxo$H)m?Z){JrK% z9+_>Pe>!CoXsn*6Jony|vRVCdwrTHG(zB8-?axnVW@ctv)nS&mb~Iy+LrGt(|zv9ygzPdmF(xhfZsZ;RgcyDJpKKbzbH6-D>TQi z<7DjO`_pw6sY;qV{S6{gc0&eVN| zs;YLnUfcu2x>IlFKMO41TUFZ5R~Emoh&Nd3?X9f~pXOXYc6QH&uXlU?>=LUK@$U4G z4cu4EvNk*W`@D>z%<@0~j$YikH|p=Q!rski*R;;eG%}8^JT-q)r=+HlwD`LE!ie*Q zY9fc{{tK2{8)cDY_I6c$s(;Ns!y*ZB^NR~bFW!{26p{TlKTl1di0i0~)T=ME5{kaw z|KWW4srau8N`Lb=X|`_QNq=m6OwP8f=+TqR+uL%2^46@saLcdy#Mk;9tFk{&CLDZs z&NWpa{^#lPNg;~h0AC#Pyk#o;(GJb}l`EYZCsaTFw7bFK)N6wP%L`&W+}zi%T{JT{ zPWI9?G@Q7}Xxh&3Gy{3@t>0bUk6&;PzPHOkFtiRlSiL8|Dokyn$DzZ@8b$_&E00Z? z^y=%q+n_w_<^-DUObiEc6%rH`BSGNS9?C^ zm+sz{;I$`Y#nHt})Pk$d|C!e>Wj*iJfe#Ctxo!G(DrHrtq+Dq@c-YDLaMiaL+w#74 zDXn+7zwz~a*Y4?W?`_=b{CgV@!-3_WKjyi4Wb!-Lr|_;ey4A$$C->l-f%s?5fE6Kk zk}khDub!4Tk%h^Tb?y48Q&oe*+WTb|zp0*nZf=*fqpO26r&%($-`P{IQZu3s)wL{7tYtgpL8@+tp+kY0< zzq+;b###5XI$P?4rW{UVV6fA?vsP-sDGyK2+L!O`Q!Y5TMpsT28Y&P6~1Ru8`MC<2@U=JbHl+y zxChc1a1SGZM#pOE1r)+m9UTvrzq{An)dkYcpvA*Getq@?84(v^qpTFiBI zdqKlXD;Qo?Ub%PncKH?)({_H@C5sn3?>+x#Ui_|_uU}uAm%h59w^77PTSaBcz1-YW zKNduQ#!9YUnd;b6Qm`fcvHhc<<=fiY++0#brUz-PZD??q=n=AamnCTQHGuKTWXBW< z-jyNCUQ9e$pJHigX)}*)dyvN5B~MafV`D)sUXZjx-C@hdjdj1j#m3asm?kH``U>q| z?3m(|$y+90@b}kQ&EWKFD}%d_Ki*oHeRn~fEl8(>?-gN%vW)xRE<8;B|F8VOsY^z- zwzgq9*REeLFWB%rh->N99f6C()<(^-seSgRwYy%*M$Xh+89Y{R8id@uTfS`B(j`j* zI6$kBRJVhyT^qOi-#_N}tEZo{ym1>mxb$k-HVxnC=-q*b+0M>3<>cZsGc{fLI;g!` zb4pi)jJp3pB`X#NhK8<%&rIfl_L&NU_L(|6FW$2Bz{K9Qr9M7B_22;$2GAa8Nd+}E zRogGMDf#Ky+1bIjf*zbRG%uGBkT?Pg+z^J+yU*O-Jv)2bXy%&O=E79}`T03{tD-mO?p9ar+7+%}E%o9x#IbGOovSBZ*|Mdlhv$1^ zyu9re{)>wB3=9k*0x8hVJ1`n;6AwGiMBMUU96A){@KRMx&F;geb!WaGW@2F2>fFFF z-x6ggjCrZJuy&ce(W2`0`}XzA88a|^`0rf*GH;5kh|H%iFO6p{xjbFgRcT_;#0Z^l zpXwyE#kgCYs;zIQ`OfBGVd`6Y^kDBMN7nhDPrjF1%0DM}UDX!(=Vxpyeq^klcURqC zCVqLDZrkSeef=v|1;?gdS^MbO8nNwp_it`4zrSaG`t2JL!TDun+n)dIJ{`WUudnmw z-sI@lVY>Aa3=BW+6rNQGl6v-SZuRlL`8+&ab-(o#qLl*mM2{bRYhR&ap>pNasi1I` zUDfXozBGPrF^y@8Z;HdU)4wxU9k;B1DCF9Hzt;HAdERVqQQ1kZ-P8Z>%HRa=Hpvo{ zk}CW6NHuFpdiBc%>-^u(JKiT_U~D?u?DO%S9v(IZ2D|$57-p9)zP)>DV`iIY-&|Dr zFev{1aS=y8Ig#qdhz|RL;U==r3D+(*^4w;-~IcO zUHqT1d1d3L`oHXEIrZ?(67D-{p1uly=PYGXR+@V??-ReVl9JL?Z+>~(Ya4^VtF2qR z^2L#rmq9z|czA-Q`~r`z$)4Tj``eFOOxM%TFK}Mj>u+;otG;C3*|RnK^|!^x`s)Aw z;yrwJ?O{&qH20Q$r*4>O*F633!1lB9(T6v?F1`LOT=uVCWZ{e2$CJ0^rd5OlOiMbk zSGcp@pELTa?}u9x6@O26IogsUI-`C<{IO}754GD^-^WhKKj)$~^_R_;w-@%!?d$V< z$f}*!`@*4PpCtN*iPev)ztw)3;kK{j=c?mBPMUnyxB6~T^@ioU<@EM0-4oa5{N~?N zRsZef{SBVo5AXMB1r+ni*xXqdynUJOon_}|O`bY6^vR#yW$#o3INnuPitFtv{4D1C z`|1Hj$(f)zz>wM)<|ARc(Kl}16btv_blTUj{)OlV);A7%OAo|WR2;dSUUxct-OF1` zKP!A+uNfrs*f8*;Z`E%bnYV_48Up8L8J3<%_`bRxI;12MtTp%NgEM{laBU0t-e`^4Y-yWfk7i~EZjsi{q4WN28m{)x70gwz-3aG_s?R>jvQ z*Y(TF%6dg}$#|+vs0acN7VWv@?U)lch-%6WhHvFFCb zn`vgVy54u)Gd1o0p&**=68L!6Rqc#~{p;C1mjrE#p84wi%&SX_m(RRhzo5@AzUt*`s z%KFRbrToqh1;5OAc)sN3*5~A`$u?kM2>4paE3os#xoIt}?ircc;=&56v{>J1oJ+8t zGe_p7t)5=r+tTd2s{#+RO}X#oJ2ie6Y|(U%rfP*oNy1|1Lu_1JiU$?bmZ=3!4E(Yv z4mw6;wgds!pjG_tIW zJw5Z@tD?mjql;X=Wk8y9&P7;5ycNGf!>SUt9N`&_Z-$B&jv)0o_+ zd|dQ?!rNz`pVW7nxISLi-~V2tpL6ESnPJn!OrjbWw76YObY+o#w(NVuv|m?F@?39CGpVSQ%#gc%@a;XXPimR}&m5mVS$>}X zG=J@cTQ#XS3OfoX7VFJeTd$Lc+q_;#1RmL+a1BeyosxT=r$v_Joxyc9>^DPVZo5V3=?H z^-oD)nSr96qt@AMbCK?&J%64@9$6~l)qRWiW_{lZjmqRjkA&KxOGyK{InUQ~8gG?O zOG|xw<17E)KkjaBntE$fXF?9)SZCb{IfUbp(GBnE>*Xv;S}x`Aeft()_U3`1+r1eQ zA|h*S+P7@kq8nW?&up*FJ2^Hj9eE%n}Sne8j>o$nuVjk(srWW91p$N8YYw#9*W z=0vXAfAW!6XujjsFJHe12?>juTkzDyALRP*y?*YTNkOM?t~_z6c%Jx+=u`%V2b&L= zHcJRe9gCYUAtvYdzOGJRgFQJVwKHqvNuOOpK`MA z=C>&NV&S{H=xS7?pWl7Ey>UPHhfVqR`u6h+6O)6}&;0s#TTpZ5O6{<9ogF>9iXR(A zzk2$%esksPvQu(fGcL|K)NwX?dmz&ehfTT9-`Q%1uU~xOIe4Yy&4?=|HHGOloAZ9X z)Vy@tUD%av&#U+To+X*OyULYbzL+Jk{a>4>;iTWoC+WzTtd8WmdFjD($?|^n)Fmf! zUS|c%R6dUoQWd|sS;F!a(n`;><^TQk>a(AOT>c1M)@e4)JgfZerI~w~oWFiQ<62mp zw(^Hs=#*9WnpSPieO>n9!^M}IpYcc=X=qqHIb(TV$^CcsX|t;LH}^(ARN82f_h*TG z)b^~KJ1>j#Fff3tXY>4fc6FaZF8)^d9+x2^yNOZh@1||}*Pq?1)#qYadHv9$s`~5a z=87y&T&dR@wD3=Szuesy$7dLMipb_g?Di_u{CMdox6LvCnSU?ayjmB&{@sOx?@ouW z3-r+t>g;>L_a3ONEwWg{>-LVVaoZ{$zSq7UAmhJx>zujedB11pE>fMW z*0cD{txck+TQSCX!?UeQ(buoNv%UD_ z(z_pjyB8N%Z~f(K9O`pDZ-L-bZvDT)=eK#ke(|}g*Eq1HKc?rqMD>>&Cp#xE&%M2= zv(I1T@Xj?C&qxM4A3pT|-ln;GrDmdIdY`d++r#jX=eeeJN>}*qTk`CCF zZn)ZQztYK*Cuhx?<$7;=L}}*44?547p8ebOVE;EzH$&Wme>q z%F>I5hHSrnO^FJVt9X|G@i6zv{W4q6pPRB&#e>&}ro;CV@ zO8V(T^koQLdvY)I#s1fc+LQCWZ>{XD?Wqh53tk034?Ximc(sM$>MctHv_xcX+=vj5 zTq5&9)S>eJ>g#z1JIm873tyFNUgpcyrAmTDil4Q)}J&~*=1(fh?eSN?;he#vKAHgo#ivaGDm%>NyHU2ksJ z>~BxC>$$XNn`NG1U^tNOIG0f=#6(&7u+-BI?-iZ@k9htJU8gjW&s2F+dC|HIt127k z%lUu8V!o8*qG**Cd=)>}oasxgtzRF);BK`dQmg`%0eX{2Y(-Et#wC@q^ZgpC? zcW&su2$YT`Gae3mw$V6*ZlqSn@4RMK`s&5!Pa3k z^Ub@x>~Z((M80{dO#1e7{tm6a?%VYrI9l8M{Q5FiW}2kAxTNggSG&W9`_e&X`xD!Po3@qjXZ%w2^#8B3 zv$yA%?_C$OciD_@pX#Tsj^D|jdEv&@t)aV5+MijVvwjh1JGs=)OHWtJJyySbX8(&j zk6&yQzVke$u%@P_o#+3$#LLriZRR;T{C>0V@&-TtF|VIj`^!A%Qqqs@ zi8~*cQuHODu?brLoWstqc+X`~+vX5PfD7@1byZdka{%iKbzc7Ud%v93 zqup%0ul^gA`{m@kS+;*$z=U1-@AbBs*w{RJX}sU`^}iR_zt<;ita$#e{#b?2o`QEG z{p$97JIntcd^5kkaAk1yi3N?02NkW$-|O1^ul~OG(W6H%MTLd4lVh7fzK@Avu6NNB zj4LbqHgoelvuoG-`uLX472ENl_D{>Km)F-tovT~AZrxdp&(kN)bYx80yG!qN*!l&H zfA`6$O!T^$mpa91wozu&@fj1}h&t8S<-h&a>*#p!+qbf=!d)frOp1T;U%B}5bl=^o zZ%btqSvx?!{FZw=#N1rp;KHJ+QoYOX?v~d#WSs|fG)`tt>^rxr6jgo$j|aHtt*7%e%;# zfq@})>F%z`t#5KXBPv>^Z7XYJ5WI5qBMXM?Jm0H(pHG;F!YL*4Fm;SRZJ_Qo+8ItwU+zxr+XaR}P5F z&p+V6D)Geb!NbMJ+xukxP7liFQ5LD^I$qr~)2`HLdM3(FJ(iS-2j%C_FQ3P-ROSB9 z-z!zW&Ym}K-qg9V2@BSFggnsmaGeA_`tM7ioW|*in!7SNZ?dH>dXW(56!>#axE=rV z{Y(rD1xx2K1U6q>a6kFk-+q(Zz0*>+UcPoMZhzX-RZqR04`t>wb zKX1W#lY`HC&s#pTDEYMt0|P_B!g&l6i`pMP6bxE9#n-X(Xj0Ynt4Hr{xccVO)74js ztTG;4=lk~U%cX0V_SHQO3kcXyU)12yICsm6?4o9-=BRBYsf(S3)|GG?yB|Fpxi#O7 z=kKR(K2|m>>*+EVg^Z1Z_2uWEIiqtbBPid+<;m>r`O@ZDOV+Osf3)RSRTXDvr=+B$ zNN8|=fk(!~#dAOJap=5pZtgzNV$KJ)>ll+fG9r9@bV^%V+b{2)f1mr;T~i~I`UHRT zHL<&|&EEe0#rAE1huKzUW}4^UH8Z%dJ~8!ZYSe>skj*%qoSnK+Cd)6Rf4-JGWtr{- zyOLEeD}KEA7@B=eQd&AV|GM7Xo7x7iuCFhD{A?|Ad*6;LEB`)uxj9+g&uD4LojIY! zx%YZbvz~T+b$$E!h04d5otyFX?{?*&p!(qOH8H}1lBLg&rRrVYhNYEv=yAuC9eQ%s@zBjn#I9sL2;w{jIoiMmz2Wr=;Z&^7{Y{zVq zM46}D;GG#Wq`*5f8t2Yk`^myM-G`UAp8J3G>9S6jLr?0?=SXZX`3T?o@%rj&>yiz> zKTXY^aY-ZZoapl2=+~*;;{Wy)@yb>QVPn`+vxhWWl#e zpS~((JywZmGBG%Gwn?2oJLzN!q{(DuWz}P~hnu6mq+~<7!u~H`O0qJ|HkCY zYQ3OEl>zmO&M^os$XXeH{b<@N5xwbq7#JA->|Yc(Gyi`hbTWg#oF7yR6FlmPoiQnr zg_ZUG&T4%vty!Rk!H+iz4m#bB)%|&8Kh~Z$ldX&0G6O8WXUaRf4#$Iv{`2eY7JFLN z-V9U`(iHDJnxy}}ePhzb0F5o%H|Jj4(&?a}vFXaigZnZkAL~*#T^*45zIg_YNsb4_ z2ThwbCZEi^zq9o#so3Jw-trp3+4xwJfbwvnA0Xw-f|(WClVjv^dC{{H@!`S@Db^1vf1 zA2~VGmMzOE@sN>ubF(pLePg5DR?W>96*q3)y!88#BQDFjxH&kMl$DurfFk8bPQh7) zZCN*+oShf%2=bEu#43MIQv@^$x$XMeQthV`A3uKl^6rHT6IMQ&G-+~aX(=eu8_siI zX9#pHSutVm-1^#QYnJzJ-MV0c=RKdZo5z zU48WArSWt9m@5xHuGlFFo*?CrEvzqC>7umoMSxD<>+k#Tsa;)V7<&KJ z*4OXtKkTb|Ij4EAEi(fH!-t&#=X*DS$37N0SEcWs8ohLB(94&`&y|#vs=vQVOin&q zBc^fV>hF{5mG2yLf_t7uwR?jY1&zlrgbDy0co%OtXyK)m2#2-G@HTT=OyVd#k_H5m@ZsLq7e=dACw7zia(xF3(tb(Un zK6}Q;%+wt7>I$ezWUv#DVRq^IAZK5>>P78?r@r;?LEX|tn}g4Odwc87?)R>VN?(JX zT;E!(e)RAcA)(1HY)wph;<_w%Y|V?j%Ixw@vek870Ve|kLqO@&cadjS1qK%t6>VC! zY*}9)-(tS0-y+YXpPy@A@_Q7>`{s0=G=Fq{5wZ);8O zdi>~{yp7Ysg_9>IM`cO%N(u_zT%Rw$ZOXCpXHK1xHdR;Gv2k*bo((E&8oWBQ8!CRE zvz_0*Z{6A1nOR&bcRQ?H|30}(ft_Ef{(#+#8B8aw%ip`Zx`waM^V{1iZN5a@`q|dk z=E_Qot4iLV0ws8drANNXxHLB0-=9C{0RwZS*P6Ej~qNWPc|Z9OZ4{l zcD3Ku_`jcLW_Had^xN}z(2-JqE_}cE{}+GGjV(_v2;LXZaNdtH%~Jn~-&kY*s+g)D zFE;kg-DO^w>+7oSH}~D$e20XD35%{AKVE*~!~2YFhz9>BkVS7UE%tx>Y;A1xc6rmX zKd-OngNhS|t&gNYg~6e7=e9u#gBsWM4f+Y}TK%6deBZeJ{NY`CdSTPVd_=|NVY?D}10FKtfz&F&I8Uf~n6Mt|+IjQ&oi#)4}-E1lVP$M3ECyE`zsxTr{Esd4?E znN06CE_Q$Q^7Y!7z3*-s@9#O|v+c_L|6eZ%-cL&H1`zMr z5{(!sNzL<%0%M;3|KzAX-pI&E z*tmXQJIc7e|9ngL_tj;&+t2V$sh{=z^}fICqMPsTe$P;Q-Fn;Kv&)iQt*rLzMSZG& z^i+Gg)z+2Qzkd|YdKAAq%W}76OziBRc2#pG&Rn>3=}A!Rukd`iE-6gcI$B6fZsF3k zu_+BVHoI?^ySw|?kt6f$s^86d-WsU9|LNzSKA~)?dx0keae-PZ6udcht@FmPwUu3QS5~iHt}QPibc(IR zua7S>^5*Af`X#yddJg%fFQ3(w3UW+BueVli7RZ zkQ(Viy*$byur@lVEM8y-8itnOHM2=dR@)YRQOh^dIZMLS#3ZG<)Wzz{Jl&INtBhPN zA{Na4^Xs;dZS2Lbr{}%VdT(ElaqiB~uAqg>43uqS)h2JeADR@j@599U&~7EJw3gxt(BF9?d`wx_ieqie|>r9xzYtx+d24N`K%&(N>=TNy2sL> zZQ(j%mv^tryEk{s`8l6$YOe(w7S+_)OqPCa5Gu3v&nDTxz)#N-{(cuPWM^f&x-vLg zP@w(!{+5}yrWkMDRQBdq`=L`OPSmeheB4iC`YiOpWv6H0!DW^G_m?#9{X1ROx}rx? z_y3Ls31D~B?no1Gb~>yf=pmc<@Zpzt&%ZY**(f_Re=9kBYisu2%HNMoy~EdCD=ArUEq#7_zmIRd?VGUNkNa6osC`{ zYFxH5xLtQu{Qi5rv$tzZDqb1<`{d>H<9>5O<`r!C-JtB=e|d{`_}ZyYpL}_H`SsNQ z*{+%U_to;rTAGy}Tl0Ru?8Hg4b|pQX)p25lwYlErrE&M{W^d2)_wmTNxTw<2?bx%m zw>5mbzkuieSBPI{_;_~q_Wt=cpS37u5K@MKelSzu8PLvKQuSj&vL)} z_0?5b$WH1mPFJOiAC=kH-QO{F#*F$GxAxqeyZbwzyzM%T+{f3dZa!+=zdpwKqS<8W z==XQNZC|%eFWM^eki{u+z37cyxmV1LjG}(sW)j^l&&0sMpuybB^!)tPb{{owKfh~N zu3WizQ8PwsqDN3r(AKS6cbMz#=jH&lu(jsz+^_Q^|Np^;>*qCl-8)0MIy4tXB+U6x z|AC{iAwMr~T~(Nf^}nr^ujfjKuUVrb%Di%;9kkuOrsH)5{7l+giz{uu&H;7x7#1vm zHn%InkEnC9xVg2RdEVM|_fg)96`=9JpVuMdfd>%dfhWTuC8PyO^(%}W z{`_xNx+`_`c)jYPwjaXkPp@lsyI3`|)!7#R;s=$K4oerFQ%l>rdW-KFPysOCaLz%H zW(F)4ID?X3zZkBX84l|Bqlk9`fS7LomB^QAQRo=($!m@~nu+tE-nkvx)cc(AazX z)(!CP)_}YiG7m2{pLp5$gS^T2uk*HS)5Bled4{mXtYTz_uyhVAJf`xew(5mw0Bvwyx) z^?xaFfnTers92wW^W3+z)YQzZETdP-V#i;;eyt2z>%}^#3y?xftD;J^a&)g;{oq9tYJfIMbG!D<%7u7 toB+4O9dI48jC*|+*b}(UXg>d+@vd+Xx9Qi*>z9HY@9FC2vd$@?2>_s4PGkT8 diff --git a/native/render/src/main.rs b/native/render/src/main.rs index 53d0772..cc4a5c0 100644 --- a/native/render/src/main.rs +++ b/native/render/src/main.rs @@ -2,6 +2,7 @@ //! //! albedo-render FILE --start N --end N --out DIR [--columns 79] //! [--tab-width 4] [--language NAME] [--max-rows 80] [--max-images 4] +//! [--no-line-numbers] //! //! Writes DIR/view-1.png onward and prints one line per fact, for the caller //! to parse from text output: @@ -32,6 +33,7 @@ struct Request { language: Option, max_rows: usize, max_images: usize, + line_numbers: bool, } fn main() -> ExitCode { @@ -59,6 +61,7 @@ fn parse(mut args: impl Iterator) -> Result { language: None, max_rows: 80, max_images: 4, + line_numbers: true, }; let mut out = None; while let Some(arg) = args.next() { @@ -80,6 +83,7 @@ fn parse(mut args: impl Iterator) -> Result { "--max-rows" => request.max_rows = number(1, 1000)?, "--max-images" => request.max_images = number(1, 64)?, "--language" => request.language = Some(value()?), + "--no-line-numbers" => request.line_numbers = false, "--out" => out = Some(PathBuf::from(value()?)), flag if flag.starts_with("--") => return Err(format!("unknown option {flag}")), _ if file.is_none() => file = Some(PathBuf::from(arg)), @@ -151,13 +155,17 @@ fn run(request: &Request) -> Result { Language::Unknown => "language plain unknown file type\n".to_string(), }; let mut painter = raster::Painter::new(); - let gutter = end.to_string().len(); + let gutter = match request.line_numbers { + true => end.to_string().len(), + false => 1, + }; for (index, range) in pages.iter().take(request.max_images).enumerate() { let page = &rows[range.clone()]; let path = request.out.join(format!("view-{}.png", index + 1)); let png = painter.png(&raster::Page { rows: page, gutter, + numbers: request.line_numbers, columns: request.columns, foreground: colors.foreground, background: colors.background, diff --git a/native/render/src/raster.rs b/native/render/src/raster.rs index 229e37a..6a4674e 100644 --- a/native/render/src/raster.rs +++ b/native/render/src/raster.rs @@ -20,8 +20,10 @@ pub struct Painter { pub struct Page<'a> { pub rows: &'a [Row], - /// Cells reserved for line numbers, so every page of one view aligns. + /// Cells left of the code, so every page of one view aligns. pub gutter: usize, + /// False for text whose own line numbers are not the source's, like a diff. + pub numbers: bool, pub columns: usize, pub foreground: Color, pub background: Color, @@ -62,7 +64,7 @@ impl Painter { } fn paint(&mut self, page: &Page) -> (usize, usize, Vec) { - // gutter digits, a gap holding the separator, then the code. + // the gutter, a gap holding the separator, then the code. let code_x = PADDING + (page.gutter + 2) * self.cell; let width = code_x + page.columns * self.cell + PADDING; let height = 2 * PADDING + page.rows.len().max(1) * self.row; @@ -73,27 +75,20 @@ impl Painter { canvas.fill(0, 0, width, height, page.background); let number = mix(page.foreground, page.background, 0.5); let rule = mix(page.foreground, page.background, 0.85); - canvas.fill( - PADDING + page.gutter * self.cell + self.cell, - PADDING, - 1, - height - 2 * PADDING, - rule, - ); + let x = PADDING + (page.gutter + 1) * self.cell; + canvas.fill(x, PADDING, 1, height - 2 * PADDING, rule); for (index, row) in page.rows.iter().enumerate() { let top = PADDING + index * self.row; - if !row.continued { - let label = row.line.to_string(); - let first = page.gutter.saturating_sub(label.len()); - for (offset, ch) in label.chars().enumerate() { - self.glyph( - &mut canvas, - PADDING + (first + offset) * self.cell, - top, - ch, - number, - ); - } + // A wrapped line's later rows say so, since they start at column 0. + let label = match (row.continued, page.numbers) { + (true, _) => "↪".to_string(), + (false, true) => row.line.to_string(), + (false, false) => String::new(), + }; + let first = page.gutter.saturating_sub(label.chars().count()); + for (offset, ch) in label.chars().enumerate() { + let x = PADDING + (first + offset) * self.cell; + self.glyph(&mut canvas, x, top, ch, number); } for glyph in &row.glyphs { self.glyph( diff --git a/native/render/tests/cli.rs b/native/render/tests/cli.rs index 5acf5ed..46d0524 100644 --- a/native/render/tests/cli.rs +++ b/native/render/tests/cli.rs @@ -1,4 +1,4 @@ -//! The binary as albedo calls it: a file in, PNG pages and a text report out. +//! The binary end to end: a file in, PNG pages and a text report out. use std::path::{Path, PathBuf}; use std::process::Command; @@ -43,7 +43,10 @@ fn render(file: &Path, out: &Path, args: &[&str]) -> (bool, String, String) { } fn png_size(path: &Path) -> (u32, u32) { - let bytes = std::fs::read(path).unwrap(); + size(&std::fs::read(path).unwrap()) +} + +fn size(bytes: &[u8]) -> (u32, u32) { assert_eq!(&bytes[..8], b"\x89PNG\r\n\x1a\n"); let field = |at: usize| u32::from_be_bytes(bytes[at..at + 4].try_into().unwrap()); (field(16), field(20)) @@ -150,3 +153,23 @@ fn bad_requests_fail_with_a_reason() { assert!(error.contains(reason), "{error:?} lacks {reason:?}"); } } + +#[test] +fn without_line_numbers_the_gutter_keeps_only_wrap_marks() { + let scratch = Scratch::new("gutter"); + let file = scratch.file("a.diff", b"@@ -1 +1 @@\n-old\n+new\n"); + let page = scratch.0.join("view-1.png"); + render(&file, &scratch.0, &["--start", "1", "--end", "3"]); + let numbered = std::fs::read(&page).unwrap(); + let (ok, report, _) = render( + &file, + &scratch.0, + &["--start", "1", "--end", "3", "--no-line-numbers"], + ); + assert!(ok); + assert_eq!(report.lines().next(), Some("language diff")); + let bare = std::fs::read(&page).unwrap(); + // One digit or one wrap mark: the same width, without the numbers. + assert_eq!(size(&bare), size(&numbered)); + assert_ne!(bare, numbered); +} diff --git a/priv/python/albedo_plugins/files.py b/priv/python/albedo_plugins/files.py index 5c43382..6557732 100644 --- a/priv/python/albedo_plugins/files.py +++ b/priv/python/albedo_plugins/files.py @@ -16,7 +16,7 @@ import tempfile from dataclasses import dataclass from pathlib import Path from collections.abc import Awaitable, Callable, Generator -from typing import Iterable, Sequence +from typing import Generic, Iterable, Sequence, TypeVar from albedo_api import PythonApi, ReadyList, Text from albedo_plugins import bash as jobs @@ -67,25 +67,31 @@ class Rows(ReadyList): __str__ = __repr__ -class Search: - """Work that runs a supervised job, so its result exists only once awaited. - Using it without `await` explains that instead of printing a coroutine.""" +Result = TypeVar("Result") - def __init__(self, call: str, run: Callable[[], Awaitable[Rows]], result: str = "rows") -> None: + +class Search(Generic[Result]): + """Work that runs a supervised job, so its result exists only once awaited: + `Search[Rows]` awaits to Rows. Using it without `await` explains that + instead of printing a coroutine.""" + + def __init__(self, call: str, run: Callable[[], Awaitable[Result]], result: str = "rows") -> None: self._call = call self._run = run self._result = result - def __await__(self) -> Generator[object, None, Rows]: + def __await__(self) -> Generator[object, None, Result]: return self._run().__await__() - def __getitem__(self, index: object) -> "Search": + def __getitem__(self, index: object) -> "Search[Result]": """`await files.find(...)[:10]` slices before it awaits; apply the slice to the rows instead of failing on precedence.""" async def run() -> object: rows = await self._run() picked = rows[index] # type: ignore[index] - return Rows(picked, truncated=rows.truncated) if isinstance(index, slice) else picked + if isinstance(rows, Rows) and isinstance(index, slice): + return Rows(picked, truncated=rows.truncated) + return picked return Search(self._call, run, self._result) # type: ignore[arg-type] def _unawaited(self) -> TypeError: @@ -186,7 +192,7 @@ class Files: def find(self, pattern: str, path: str | Sequence[str] = ".", *, glob: str | Sequence[str] | None = None, context: int = 0, max_results: int = 50, literal: bool = False, - case_sensitive: bool | None = None, hidden: bool = False) -> Search: + case_sensitive: bool | None = None, hidden: bool = False) -> Search[Rows]: """Content search through ripgrep when it is installed, else pure Python. `path` may be one path or a list; `context=N` adds N lines around each match. Await it: `await files.find(pattern)`.""" @@ -240,7 +246,7 @@ class Files: return Rows(results) def paths(self, pattern: str | None = None, path: str = ".", *, - glob: str | None = None, max_results: int = 100, hidden: bool = False) -> Search: + glob: str | None = None, max_results: int = 100, hidden: bool = False) -> Search[Rows]: """File names, not contents; the same ripgrep-or-Python split. A pattern with *, ? or [ is a glob over names; other text matches anywhere in the path. Await it: `await files.paths(pattern)`.""" diff --git a/priv/python/albedo_plugins/view.py b/priv/python/albedo_plugins/view.py index 1003810..acaeeea 100644 --- a/priv/python/albedo_plugins/view.py +++ b/priv/python/albedo_plugins/view.py @@ -1,4 +1,5 @@ -"""Source ranges as syntax-highlighted images, for reviewing code by its shape. +"""Source ranges and diffs as syntax-highlighted images, for reviewing code by +its shape. Rendering is albedo-render (native/render), run as a supervised `bash` job like the files plugin's searches; its pages attach to the running cell. @@ -16,7 +17,7 @@ from albedo_plugins.files import Search, _missing, _run, _which # Built by native/render/install.sh next to the kernel's own files. RENDERER = Path(__file__).resolve().parents[2]/"bin"/"albedo-render" -ROWS = 80 # rows per image: legible after providers scale a page down +ROWS = 80 # display rows per image: legible after providers scale a page down IMAGES = 4 # the kernel's per-cell image limit @@ -24,46 +25,106 @@ def setup(api: PythonApi) -> dict[str, object]: attach = api.attach_image def view_code(path: str, start_line: int = 1, end_line: int | None = None, *, - columns: int = 79) -> Search: + columns: int = 79) -> Search[Text]: """See lines start_line through end_line (default: as far as fits) as syntax-highlighted images, returned to you with this cell's result. - Lines wrap at `columns`. At most 4 images of about 80 rows each; the - text names the lines each image shows and any left for another call. - Await it: `await view_code(path, 40, 120)`.""" - if start_line < 1 or (end_line is not None and end_line < start_line): - raise ValueError("start_line >= 1 and end_line >= start_line") - if not 20 <= columns <= 400: - raise ValueError("20 <= columns <= 400") - return Search("view_code", lambda: _view(attach, path, start_line, end_line, columns), "images") + Lines wrap at `columns`. At most 4 images of 80 display rows each; a + wrapped line takes several rows, so wrap-heavy code fits fewer source + lines per image. The text names the lines each image shows and the + call that continues. Await it: `await view_code(path, 40, 120)`.""" + _check_range(start_line, end_line, columns) - return {"view_code": view_code} + async def run() -> Text: + renderer, target = _renderer(attach), _existing(path) + if target.is_dir(): + raise IsADirectoryError(f"{path} is a directory; view_code shows one file") + return await _render(renderer, attach, target, path, start_line, end_line, columns, + [], lambda line: f"view_code({path!r}, {line})") + return Search("view_code", run, "images") + def view_diff(path: str = ".", start_line: int = 1, *, staged: bool = False, + columns: int = 80) -> Search[Text]: + """See your uncommitted changes under `path` (git diff; staged=True for + the index) as highlighted images: each hunk with its surrounding lines, + + for added and - for removed. `columns` is 80 so a 79-column line and + its mark fit. Files git does not track yet are not included; view them + with view_code. start_line counts lines of the diff, to continue where + a call stopped. Await it: `await view_diff()`.""" + _check_range(start_line, None, columns) -async def _view(attach: Callable[[bytes], str] | None, path: str, start_line: int, - end_line: int | None, columns: int) -> Text: + def resume(line: int) -> str: + return f"view_diff({path!r}, {line}{', staged=True' if staged else ''})" + + async def run() -> Text: + renderer, target = _renderer(attach), _existing(path) + where = target if target.is_dir() else target.parent + with tempfile.TemporaryDirectory(prefix="albedo-diff-") as scratch: + diff = Path(scratch)/"changes.diff" + git = ["git", "-C", str(where)] + changes = [*git, "diff", "--no-color", "--no-ext-diff", + *(["--staged"] if staged else []), "--", str(target.resolve())] + # Outside a repository git diff compares paths instead; ask first. + command = (" ".join(map(shlex.quote, [*git, "rev-parse", "--git-dir"])) + + " > /dev/null && " + " ".join(map(shlex.quote, changes)) + + " > " + shlex.quote(str(diff))) + status, output = await _run(command) + if status != 0: + raise RuntimeError(f"git diff failed: {output.strip()[-400:] or f'exit {status}'}") + if diff.stat().st_size == 0: + return Text(f"no {'staged' if staged else 'uncommitted'} changes under {path}") + label = f"diff of {path}" + (" (staged)" if staged else "") + return await _render(renderer, attach, diff, label, start_line, None, columns, + ["--language", "diff", "--no-line-numbers"], resume) + return Search("view_diff", run, "images") + + return {"view_code": view_code, "view_diff": view_diff} + + +def _check_range(start_line: int, end_line: int | None, columns: int) -> None: + if start_line < 1 or (end_line is not None and end_line < start_line): + raise ValueError("start_line >= 1 and end_line >= start_line") + if not 20 <= columns <= 400: + raise ValueError("20 <= columns <= 400") + + +def _renderer(attach: Callable[[bytes], str] | None) -> str: + """The renderer to run, checked before any work that needs it.""" if attach is None: raise RuntimeError("this kernel cannot return images") - target = Path(path).expanduser() - if not target.is_file(): - raise FileNotFoundError(_missing(target, path)) renderer = str(RENDERER) if os.access(RENDERER, os.X_OK) else _which("albedo-render") if renderer is None: raise RuntimeError("albedo-render is not built; run native/render/install.sh, " "or use files.read for the text") + return renderer + + +def _existing(path: str) -> Path: + target = Path(path).expanduser() + if not target.exists(): + raise FileNotFoundError(_missing(target, path)) + return target + + +async def _render(renderer: str, attach: Callable[[bytes], str], file: Path, label: str, + start_line: int, end_line: int | None, columns: int, options: list[str], + resume: Callable[[int], str]) -> Text: + """Render `file` and attach its pages to the cell; `resume` names the call + that continues from a line.""" with tempfile.TemporaryDirectory(prefix="albedo-view-") as out: command = " ".join(shlex.quote(part) for part in [ - renderer, str(target.resolve()), "--out", out, "--start", str(start_line), + renderer, str(file.resolve()), "--out", out, "--start", str(start_line), "--end", str(end_line or 2**32), "--columns", str(columns), - "--max-rows", str(ROWS), "--max-images", str(IMAGES)]) + "--max-rows", str(ROWS), "--max-images", str(IMAGES), *options]) status, output = await _run(command) if status != 0: failure = [line for line in output.splitlines() if line.startswith("albedo-render:")] raise RuntimeError(failure[-1].removeprefix("albedo-render: ") if failure else f"albedo-render did not finish: {output.strip()[-400:]}") - return _attach_pages(attach, path, output) + return _attach_pages(attach, label, output, resume) -def _attach_pages(attach: Callable[[bytes], str], path: str, report: str) -> Text: +def _attach_pages(attach: Callable[[bytes], str], label: str, report: str, + resume: Callable[[int], str]) -> Text: """Attach each page the renderer wrote, in order, and say what was shown.""" language, shown, rest = "plain", [], None for line in report.splitlines(): @@ -75,7 +136,7 @@ def _attach_pages(attach: Callable[[bytes], str], path: str, report: str) -> Tex shown.append((file, int(first), int(last), size)) elif kind == "remaining": rest = int(fields.split()[0]) - notes = [f"{path} ({language})"] + notes = [f"{label} ({language})"] for number, (file, first, last, size) in enumerate(shown, 1): try: _ = attach(Path(file).read_bytes()) @@ -85,5 +146,5 @@ def _attach_pages(attach: Callable[[bytes], str], path: str, report: str) -> Tex break notes.append(f"image {number}: lines {first}-{last}, {size}") if rest is not None: - notes.append(f"not shown from line {rest}: view_code({path!r}, {rest})") + notes.append(f"not shown from line {rest}: {resume(rest)}") return Text("\n".join(notes)) diff --git a/src/albedo/harness/extensions/view/extension.gleam b/src/albedo/harness/extensions/view/extension.gleam index bc42369..8b5a8cf 100644 --- a/src/albedo/harness/extensions/view/extension.gleam +++ b/src/albedo/harness/extensions/view/extension.gleam @@ -4,9 +4,9 @@ import albedo/harness/extension pub fn extension() -> extension.Extension { extension.python_module( "view", - "See code as syntax-highlighted images for a final review pass.", + "See changes and code as syntax-highlighted images for a final review pass.", "view", - "await view_code(path, start_line=1, end_line=None, columns=79) shows you those lines as syntax-highlighted images with the cell's result; its text names the lines each image shows and the call that continues past them. Before you report code work as done, make a final review pass with it: view each region you wrote or changed and read it as a reviewer who did not write it. Check that it reads like the code around it (naming, comment density, idiom, error handling); that nothing is repeated which an existing helper or one shared function should carry, searching with files.find before keeping new code that looks familiar; that no dead code, debug output, or comment restating the code remains; and that nesting and line length stay easy to follow. The images show what numbered text hides: shape, depth, alignment, and repetition across a screen. Fix what you find, then view the fixed lines again. View the changed ranges, not whole files, and skip the pass when no code changed. Use files.read, not images, for the exact text to edit.", + "await view_diff(path=\".\", staged=False) shows your uncommitted changes (git diff) as syntax-highlighted images, and await view_code(path, start_line=1, end_line=None, columns=79) shows a range of one file; both return the images with the cell's result, and their text names what each image shows and the call that continues past it. An image holds up to 80 display rows, and a wrapped line, marked with an arrow, takes several. Before you report code work as done, make a final review pass: start with view_diff to see every change, then use view_code for context a hunk leaves out and for new files git does not track yet; to view a definition, get its line from files.find first. Read it all as a reviewer who did not write it. Check that it reads like the code around it (naming, comment density, idiom, error handling); that nothing is repeated which an existing helper or one shared function should carry, searching with files.find before keeping new code that looks familiar; that no dead code, debug output, or comment restating the code remains; and that nesting and line length stay easy to follow. The images show what numbered text hides: shape, depth, alignment, and repetition across a screen. Fix what you find, then view the fixed lines again. Skip the pass when no code changed, and use files.read, not images, for the exact text to edit.", ["python", "bash", "files"], ) } diff --git a/test/harness/view_plugin_test.py b/test/harness/view_plugin_test.py index 8c21435..68ab43e 100644 --- a/test/harness/view_plugin_test.py +++ b/test/harness/view_plugin_test.py @@ -1,5 +1,6 @@ """The view plugin: albedo-render pages attached to the running cell.""" import asyncio +import inspect from pathlib import Path import subprocess import sys @@ -42,16 +43,19 @@ class ViewPluginTest(unittest.TestCase): path.write_text(content) return path - def view_code(self, attach): + def bindings(self, attach): api = PythonApi(self.loop, None, RuntimeError, None, 100, lambda event: None, lambda close: None, lambda _: None, attach_image=attach) - return plugin.setup(api)["view_code"] + return plugin.setup(api) + + def view_code(self, attach): + return self.bindings(attach)["view_code"] - def run_view(self, attach, *args, **kwargs): + def run_view(self, attach, *args, name="view_code", **kwargs): with patch.object(files.jobs, "bash", real_bash), \ patch.object(files.jobs, "forget", lambda job: None), \ patch.object(files.jobs, "preview_limit", 65_536, create=True): - return self.loop.run_until_complete(self.view_code(attach)(*args, **kwargs)) + return self.loop.run_until_complete(self.bindings(attach)[name](*args, **kwargs)) @unittest.skipUnless(plugin.RENDERER.exists(), "albedo-render is not built") def test_pages_attach_in_order_and_the_text_names_the_rest(self): @@ -102,6 +106,38 @@ class ViewPluginTest(unittest.TestCase): with self.assertRaisesRegex(ValueError, "end_line >= start_line"): self.view_code(None)(str(path), 5, 4) + def test_signatures_name_what_awaiting_returns(self): + for name, view in self.bindings(None).items(): + with self.subTest(name=name): + self.assertEqual(inspect.signature(view).return_annotation, "Search[Text]") + + @unittest.skipUnless(plugin.RENDERER.exists(), "albedo-render is not built") + def test_view_diff_shows_uncommitted_changes_and_nothing_else(self): + git = lambda *args: subprocess.run(["git", "-C", str(self.root), *args], + check=True, capture_output=True) + git("init", "-q") + git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", "root") + path = self.write("app.py", "def answer():\n return 41\n") + git("add", "app.py") + git("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "-m", "app") + attached = [] + attach = lambda data: attached.append(data) or "attached" + self.assertEqual(self.run_view(attach, str(self.root), name="view_diff"), + f"no uncommitted changes under {self.root}") + path.write_text("def answer():\n return 42\n") + self.write("new.py", "untracked = True\n") + lines = self.run_view(attach, str(self.root), name="view_diff").splitlines() + self.assertEqual(lines[0], f"diff of {self.root} (diff)") + self.assertRegex(lines[1], r"^image 1: lines 1-\d+, \d+x\d+$") + self.assertEqual(len(lines), 2) + self.assertEqual(len(attached), 1) + git("add", "app.py") + self.assertEqual(self.run_view(attach, str(path), name="view_diff"), + f"no uncommitted changes under {path}") + self.assertIn("(staged)", self.run_view(attach, str(path), name="view_diff", staged=True)) + with self.assertRaisesRegex(RuntimeError, "git diff failed: .*not a git repository"): + self.run_view(attach, tempfile.mkdtemp(prefix="albedo-no-git-"), name="view_diff") + if __name__ == "__main__": unittest.main()