From e792286660220c8ea7c8a743276b69ff58d67f00 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Dec 2023 15:54:31 -0500 Subject: [PATCH 01/96] Add contributing guide --- CONTRIBUTING.md | 39 ++++++++++++++++++ .../screenshots/staff_usage_of_channels.png | Bin 0 -> 134356 bytes 2 files changed, 39 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 assets/screenshots/staff_usage_of_channels.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..fbb2aa29488f5b62adcd35fbe3a25c54614ea894 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,39 @@ +# CONTRIBUTING + +## Introduction + +[Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every week, aggressively dogfooding our own tools. + +![Staff usage of channels (metrics were not being collected before August, 2023)](./assets/screenshots/staff_usage_of_channels.png) + +While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves, but we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. + +If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. + +## Contribution ideas + +*If you already have an idea of what you'd like to contribute, you can skip this section.* + +- Our public roadmap [include link] shows the largest, most-wanted features we plan to add to Zed. +- Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. + +*If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* + +## Proposal & Discussion + +Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. Find a public channel [link to list of all public channels] that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. *Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* + +## Implementation & Help + +Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. + +Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. + +--- + +Other things to mention here +- [ ] Etiquette +- [ ] CLA + +Things to do: +- [ ] Put names devs who "own" each channel in the channel notes diff --git a/assets/screenshots/staff_usage_of_channels.png b/assets/screenshots/staff_usage_of_channels.png new file mode 100644 index 0000000000000000000000000000000000000000..b9e607e59cfdd31c9477756e1f40283dde9ccbe0 GIT binary patch literal 134356 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

yaSU}o+QpZDB* zpQFz?zJE9$oY=p;*Iw~mvG*d8ipaX`To>x!R5|SlIemesjrfFbOpQrZgNPrW4EW4C z)f6*br@w>rKdvuz(0p1GVND!ZZN@tScm|AGTonN|e8W1b83 z8`ydN?q8D8SjYRGigB-P!faA>06wmg0+>WVm zroDZuT^ zu68D$`~t^3iN_mt-ARKt<9ecK1|8wHPuGt4CF))pKK)efi~ag4g?z_FHrzcH z9*fx*t5)!9TiT`U+*JSr_rd}$MC$jvAJ)!I$w-M{tj0I)RVynL4XTg*9D>u=Z>lOs zJayk2wym5Y<2r+@0k`0`^6uJIfDItlTrY(h`%(27<-G*7eP;M@DaHPd>9LHnSXwZm!9h-|TdVxxW4$MTdPrDlI7#b2IWX}$CT=-ntH_;W@*C9iJ`&_jQ_rSnq0R|AZTdk3k%k?DWApx;qZj8>BKv0tbALY6= zak1|k7-{>(C6it5sOH>)eu1DLrh3k++f)jM^=~X!sNYas=!4sS*t;|fIVOuI&6c|n zOVu0FVIfNff$yj(R|WUZbVFu1(A?hcSgcx}5XU;dzrUIG6rEf%#0{5;|LY3ev;-&q`Vx^_Ee6@V+<1;Ph@$CKO z+hfpr^S(J9ykm|jM6d#GUdbg&&%C|T{$j=_?{WD_0O*-^^z`hRU4{JZUj|!f!u#JK?lkc`*|PKh*p@xjrN0Fb9HwvTaMM zWwI$==+$nm?`9o5gXk(Ph^Pn1LNto}>H8Z`vd(vE!OqhU=>)u?C(7~@vl*}%^Z@?4 zMQa`UN1ldyVU1DJ$k$JI?Bl`Pmft@K)>BrtMeW|>m@bcDsh!B!wJ{0+13jBq=o1q| z3fJW73bSl9#Koum){BU!6TwR>i_4G2)bqZUZAz#szkp1yP6$V4>8MB2iQYW96_{vH zDi0D`l^FecoodQ?XW;r7)voAkqMVj9b;-jlEAmW377aH;5ce4RaJ&C?@8}IM_(9`X ze=mnGse0!JN|}qEWbcAsWknoSwi{-0obR8vfS~S^O4=)+p~GvR8Y=XDOfJ)WFLzNd zS#L+MJie^nygM+*$O|<&74utGsIa^Q#3tyHZOY(sHb+M&1sh*aMzk)7ODiW? z4Wn{2O0(Dme)MfVd`};IYEXD(1&hxN$`?;#?%9#OBCQ1)`54qvZ+VU3qF={zVk}Ne zmp~*MZm48zJ6p3gU3Fp_-!+8d+VQF6Ev+;2%)jUpfI1i+ADqJ zW`o$gwjLZVP&%zKr-KFtq>)ovR?g#l0u@Y=Tt%h=^bvO&0o?5zuDIuIA$-MwJj z@3}FjNw!K)tS93kvQL#mtnpK#SrTUyh67>5WzWm|f=9koFQ@fHA0;SigPjL_N3E|E zR$EKi>Cu<>W+kj2vf2jupf+D$!LV;oDZDOpy8*~t*8^H$7jne0=U}uIE8y6BUG57! z`{^qdMKU>06f3TCCBE|}*UP_h-?{&;MH4Buh9Z#e%{#Dh{(9xP6BZ{)CUfpGr=HI2 zV<+c*mG;OtC0!Xw=jh7qN^K{zI2-Z8x;I>D{sLREd()uTXAI7B8>Q9$gwGOqn4d9( z$v4j!DLt#oYU0B9ZD|Nuaa9zQ`->mFeZHv!ccIPEF0EN@yAM0~GIK!be?waoWS@iV z(VR9>^08zHt4LmMp50ww&2SqNb%k?g9cT_`g}4rw5KhoKIa|aLT&3c%d6k?^v2*S( z2Q*^8uRXy1>R)#O@Q1G`yHDG24V9PmScZ4)MO)h_k|w7OZc|0mlc?!~`AWCJjTo+6 zb3dNjVHKSwlyQ5zD)t)dqE?F!#I_)4*M~ag0bp-SOE!3ksKavac(^0xov65_pJOh? z{0(*}6ruFkME^8I}7gAcI>W#9c zy+lH=LLvXULh%^D9;PP8TkKiLcR)ZBRDjWX2W<$9cHo}>X=Jwz-gTi;fn@p1Z zxDfa4QJ#zq9m9@Jmy6~B{vVjrFn+biDY}cWFv&U71pQLCKxmy=kF-^**^3@1G}%*7q(*l*D&#t0E*P#*1h&xb_B|sR3)N{h`idOH zVKfImtaLDy?!AOSx*`k>H>UtUb=dx6KIS%k2DKC9d&B}=a83K%>CNL#q`c$9UIl=n z_fw<~*MG>mDK&zXy`=8MSd%hO%XjuAeDatnoz;=sNeI^gSoi!^hcLE=Y}za>G|zF0 zh8Ymuhgcv^Ul@g5cuzan1QJ~_qSrcfFodpvWh9_qc3-RB`%pYTq_*x;&v1Z_Qajf- zZ6vU9w9W2%L6*BFh$@ght3W?}&%#`yD}Ahi6RI~Hp%tcf8b-~zlcViY^9Z=@-`xiO z^*!YIL|NZKdtxG?9op@@)e&RS>2SESz)y*NPBF(nFGiPAGsr#VW;+$&p;@S7uABti zXJiXXpK1L(6gnA#3CLFp|GCrAJjHxfpvA_p4)+zPOT#n4bvvepMN%kd(PqCyZm{k z=rZw5u-ntH+3!uW)o()0&G1LlXV%c8uqciDw!>*)2;xV2KIA!n;%CCeLCRXUH(P-S zn$2oe@jjdycLYps5T(MwV;hymgwpcSkRVG)ENF^AeCE$0NNMW>nDfMUdexe2D$6N-a zVWHLzDzHLYklWPvu$Y4&#?zLh8Z$A*w}(RUZ?i3g#%)SAYUs~71f{Myf|MnMY+lTS zwlADb6ll@sCo(M6xUP2^MiM;HU63cLy9QBYxuFgFy_GuwI9)2po_PE zH=DzQ4X=lU&bEyi$7Dv;_)mvyZ)VbjU1-YL6uGBGJj-{UP~uJb_P#humQHz5wiT;$SeGz(uSdQEYyCo3u1EX2`v`tA1}f1D<8;96TbkY=jo!lNs<(c~}CEEwQb zDeTqc?>SYrvV?GYFDZfLbR;KyuWFB?GPq5QHW+oj^vn<^meB9+eNDNpuZe>>Vk9ck zsqZuCK&Dm?#9Esp-t+{~RrN%zUy)QGqQ>XDYniRyu%~O6%-WJwO_*laAi0j$rl=-i z|18#ihXw43WHwl)w>Nr?MReC$JAz;0R0@3tg&cxhwUGmz(-$EFlh=N9zGCjs%U$zJKu zne~yvP5S6*Z`r_9S+i65t75SC9iGx5!pS&~&g%89_u=ple@0sl@B4$%M~-4&SGapr zp43Q*r6)IGezq36|3~*HUm|xa#wyVx;he9Ew(|?mb%r$DCY4UR zjRo32kLI4fKZ9&4s((dHp*P&Ar{YM_dtfxtN#(z<7wV~bf^)ug!dVYoO=T(t*xDW&Y9AjRQ^(9Ul&8Mn4a zij!BN8W6-lVKhZ&dSmxwg>Mj>^=S9Z8^fPg!-;%=S~`Kv6yo>f8(ci`bocAse&&O> z2{EcJAV}s`ANRSxSoINX8TfZxXs<{oJ8Bv5(1rf+;YVv7x9*v96l%C7h%gCfF%|JV zYX8-XhS-+I;huSMX&jeP`!S=Eoe_a}Z0a{VW@dt=LLSp0Kw*{8jZU(=tyeC3VO8UD z@oFzu*i5dO4h{CZA5(5i-N1x_T)aldzc(7Ad`n77QRxN)_Q|5$b*^=YBlDsrqzHYnq1 z3%+Pi#SXp0n)6M#(Rd114^qC0Vd0|L&=UE;$67RlzC^-k0Uxsv>kf{lyUU=T&?yE5 z=!ClGlY)mWvC(@QU1@H=LvdL1f=|7hZDO=H zs%a?9(%n)C0S~2=xI59-QwH{MP$qL`ybrK~$UW!>zO`J;hnSH8%}P64WaH)S;>G|% za4P)KuoT5qxzUo2caySuto5)oP$d>Ho}^RW8)cQ{R&VW9`lJ+nQYxY^>Spp;$m#IQ zG;m;-)mmjBOtN31#y#e2yS|0}FcTr+vGY#1-uJ7%3bHJ`@8eHM92Nh)mB4rr+}|*7 zyS-ppnvqWMeRH%tQV2@4A<&oryXKD;To1T2FS$@YwRB39xM_p(8>@B>tG)+oo{?BK z1eMlX?pJ&lg?dKBw2thN7g|<7g$$zy%bNRK?>Bo&f1T+u^`-mfGJf9^omr_O0*w@W zg7|#7;a);Ux5=Z`@$XU1Hdg5NjmA!JK zkeQYBeQn|AVfkycFH5V|cGg{~+frrRJHI#Fk)q4JG6SngZ7Y8aV3KAxxhk_8Jpz{{ z`ha5;ok^a-b+QIV-6KaSZu#5QBNYS?p>p4}ejkms0Rx_p8jGuFY)FNJmrbn6G^#XFQ37TEwQ&U+_3F zHoY1~CB=Kr)$@Qvt$R3&{}cS$aN->uM)VJ%84{5}+t}3U4CidOJKm>&SMz3sS<0D3 zSu#aZnVdt1e5hZ_>EMKbjOpf;`azO$OEanS0`-mxc#hfNt`LOCCAkpmuI(f~Ur}#z z<5-l{#47j^P)d1syY8%*Mu2t4bUc(sV%gTNXLQp|Y6=%L_?0qber|1a4RXFXJifP} zKo>cKHjJ>DDejTV|NhMjH62yuBqLr9BKVeASletMoLu?xeU(%_X_kzD*PWfA;`F-1 zRp-4?G4;(V$s+E$J=MpgW2OxpVyA?a*=Xd$&Z0YSNhlfgYhB8UC@Tw)gt1=!4-*JJ z#~|fQW^XaNY5V%7NRr)~&dM#3N-X_8Nnd}78l8XHo(1P;tGA{54+f$NR)3V{SCW`R zxtHiMXOKrdW?S zva-eORYH15AV*@JC@9Gkmz)nr*Rhj!>SYUtABtqH>Cmn1MsQTYq}!OI#P<0%p}oh- z713f_M2s69<}Yuc4*PWbya?``XknJrtfL>Pk*&D!GNxjQr8ZQ`L7|ad?|V_9EMG+y zlDGC~6)xvOJ5i#NtA?t~v2O7(EqBqm+fX!Jp|pP+DiHKYpHFd7;IaUvXjKt%@C9v{ zc^(1UO`EpTX8I1FdH?b98fZL4B48!9*kEDcmSe-5BkxZaRul^*wDY+|LFK5TWw|4h z8niq^S#iFnx$irdV6fe=d&<6Hq1Z@m{1fF=p+FCJ6{Ykzf$;* z;dXYCB~sL1mj*E2D#6y`vEGjmEYmhdHc2+vHv$((kb$(zkQp~;w{ z7sh^yGlTdZUv(S1l)Ve+({N4??|)OOk2^xlNZg^FD32+aboUf8E;FvJD6sutUHK$k zzGP{1JDmyNl2F#ob zx9~cnbGi$>nK=u%Irj0(*ceO1aBWa!9!C5)FLtmCl(dh07;lSiPRvrm9C9~ULwq5* z1o%U+Y{|xnEC{czS?~E&9r4go27%;$Wv~Ltei-k^i_V!(ns5)ib=CFgMQ?UyWzA@% z_UqbUu1;zY`PDDPSlg0*MewCT=;I}g3kjs*cC40ftH2Vcue)}J<&P3nM7^-oCX0<( zmlE>10DIz>wt_#w&+xoofuG^9Z?cF_Xl)`Gp9-2#`sBbzJ`sJxCwpyXXo_K?#y5M~ zUaaL@ZP?zL!776sfc|&;a3tgQaw$f>&h&eQ0z0dt*cjWhCoWyedX2v^DJhkn$!`kx zHRh;TQP4R+(w)j=L#MvRRi&bH)-x!vj}cf3U^&Sw#=bR7Ij@B5on)BH`k5?BMNHZ? zD4`EvkO|1yI*Iy$g3>h9Jq7*JqRlFD@Q1k7Fg2&p%!9$kocI`~mbVA7Bw2n|+13ul zf~dg|_61AUI1KYV0R3zE!v^)72M#;V08kJ7@v(711)mPJ8z+cDLl!ZpaeOy=aff?M z?~Y596(+4^CyFv)!oo98B&aTEEQ05^7c6zNdf`XD`50vXOxZYkv6g2Pa45&+KV7~N z6u;zIa5rW@sN137VZTO|`SleU6}zQbhn7F;Mx}EXxOtroe)=JdlOkJLQ$YsViIRay-8hD^*}t@q5rk|=cZ6(HkSz?3J#L?F`kv(OV8gU6(50A%UK z%Nhu_j)x|`u~D`jMG)2g$m$*15b+1UT$8In6vdP7v7g~AH}{?6Dc3X5O@B4B+Qr;^G%d=2s4P9<=>P!!u@Z#bq)$C|fsHGS54LJMziCEr~J)Dy|^qHfCdn2vBV#VknF z*3YsCgcHzk@`k#%CZI@*Xq|v$elRg_p*^&sXGiLc4qcPzf=9=U>gN zdv|@=Ko^PxdfK%x>4OP>!TPQ*Fv8|QXBPvHW!hxHsi>(BBl9R&LSEWvEWld)9~qlid&?t@rS$eWB@o zZzw3^63DW|^aI00xN=eh4zv=MP+YZ(wKuP@x1VYX!t zdA|aNe0h6FPaq2|Izyk$Q>Wx-q3HO$sy^1ebm%lwMo^aU@_p&|H!$&8L0KMYS)*MR z=6uysBxxnt)*b8LBPI4lJ(dpLZM*jv6|gf~sRV2orfXay5GqH3RzCuS)YM{)bZgX6 zAeNj{5?J)HR2)}6%=Q76i9dMOIzEa#9xA8oamYg%Zt)Wc+yYBCp4sy@exx60jO|7F z6x+;H>$+`^oAcH#jio_ZLZ}m@;mbp%-mH|oIgJ!Isr*d51|J0m7z{Z_D$4(r#{&5UzJ`b8{O z)qeS2qbQf4L3^$yVAO=gjy5K}6N~!*Oh+Pm@ut&C?apT}hJLLPrl1mG{-46t z&6XxLBJ8wS)tgc~z^R!Hn}!zF!8 zNeHz-8SJhe0T&c5X7SP9IKa?90DOB~(`SNbH=S3l!l!-P zeQV0N|JGq$nrprarG25;o0&LHr~q-FkkW~f3D1Qn1zcGrc|(bVvWxhc=I;^PL zLQw&;hp%>~IlA19zrq-lNFQN8@bc}pphx8VvT!@ght#Z zHdm|2&Og!iG{4H;?>H8|j-TM9>ptTeiJB5K^23|?CKLG3{;elrJeGdHW8`6`OrY17 zMoIQO%e4ofkOI>tBaxUdquLRkr`nJwwkosFJfxo+BG~&%zb!RqH|`ASR@j|yE^BSH z1QB0Lr9E~8g~eO>ZLX@RQyqsa7Cq4qqmwXG_FO~AsAmrU6`81fg`Rw@{BRUN+PDF4 z&7$8Qb(ZOAg(538e4V+}pR<9^P&O6Oc6PXHA=O_J&|;u|cB(jrmH9rYW>~rq+2K^) zr|PFULg`;DcZEp?P^jI0`be+#xfLn2*~nz58RjZNJ!S>Vyrg6?hk#*pMU@+W=;wn; z9k3O88_5o-wMt1@OMq~zDBYPBkPohz_R!;FLs%1g5lyE=#v@h+Q?#Pna=Wroff#rxCVQrVGdy2C(}Fx4TczKXVjmIP~95DS9^gM%o4Y)c!T?WM#0~ zdZ9*91GF>mm{6cA5dB7|9<6sUy|lx8`)45b-7EMNwk{}X{Zq{`?x_!={GjZd##g}= z8vUK5sNgJ1`OhB)4uZctO~>ciR8o{cQ4c;dFJ0yc*W^r0RKZ7`m);Q6tGoumD=MVo zm;VWmg(0{H^xDNq6puumh_*gvjc=RpE7w~jq!d}aO^e`%O0ROOQ9(tOyy?=kL(r%x z_9F#GSUd-=sbjiwSUw?kJxLaQ7!O z>&>3g!Dz}nJ|{by&L6=DPr~|*K?k7JfyggT;MZd5?pdF zojX9r?~`)7-aV9xw$cUgxIp0yr1n^aBCWbnhd(2<3=U{Q2eboPs)ObZcC^f^kK)mD z+D7yMw(iAM!RAvuj$T(%TrlPxJUNE@215%+!XrZ7XPKe?8tnN!e;J&!I1cHxp<1oV ztL5O0Yya~Yz!2#<3qw+rLvb(Yaml~r#_aj$uNmXS*jH{chet&xI~V2<>#(X}%7|k} zx4QWt<1BbyjCNJzZSCU!vx=Wf`42Dt_c_vv1t%+QBdshgpXD3)>V9St5z#IiNd`D( zQ_Zr#;a2-k&-^c<{D+Z%Dc-vbH2?0#N(SBiNu@t{A>{%NOH^ICh|yfo^}oMF1p-02 zrB{6{e>|xx6u{9}z1NO^NDv^-1N7-kagsKV{FC|oobqcz(oQn2X5l|t!No6@fZiNx zB56NC=moF+XR=;=+KhK)k$}RG=8wL7!7EO{DaCA>fpMlkoYZ$DXLwZ9&3!B&a>qwW z_y=3@*H=g(z@E?BcT@fGhP=}T=o{Dd;?EX-v6hny`a;x4FVNW^O-#BM8+Wf&rTq`T z4h-801}1fqSR(%?n{hFx`&<{~Dh0TaC$jiY=XD8Njxb5gy>ONI`Z7RgtT&2(wjN;o zsB+*@rks68f50q%edXN+li}WE`AbLrchO>aG73;Fs$6*Tr#k=dJV`3NjTaFSVI(P! z?P)pU41B?sI|dsJ(C!e*2Yw+n^`qfW+tiFQ4&@z0>a-^{?Yg zN0Xia5Lm?f=&QtNSs6bi|F=qhnVUKy6pJ)LZ?A2!9wJ~wYtFN? zcj6mg#xE`|j;%ox1Y%@xPG27kF2U@_I40dUK5dG*E9)qI;J1grm?*d9Oct#mT%(qJ zmSEZfTWVKJ6=5u0;RsjH?yFK9$}@Fdo6HUp>if;gpG04*vb$fh<04D*ceOMuADxkuIvQLG}b~8--EduIoUOf_OomSd!~+;ChHw(J8 zdrX1YV*H9hG?3?ht>Vt;>*XGsSlIY*{YSw9i)UD-_NcDn*!ODZmBnq$Ns-Ni4up=p ze9if+7)jOBzv9q;;{yNt+|rf>_c1pv{mKj7&(hS23hyLNmU$*xga=zE5SVYCg6u{l z3;7nRtE=Jsw4+Ps1!_fG@YN~L;KgHzSIGba-DXbHqc9YbEt2-$q^{~^rBa*D56$wD zNEf%qq+xvxOYQ8LKr+Knf>Q3+C*~Cbia_JIdZqIhjSECKybf3IcXcVkV@gX(Oki8} z@T!>R?mq6=B^EQ&z@~+>J+ti%WQcffHqR zY(fU^Z+vz`5wc2FwBjBwSQ?K%>4rp^Y1=bj$3NN>W3X2xwo|CJsQ$L|?B#WG(;NT3 za{yY+yx{j|YC@NRTk{XaQZu5pfd<=7JDVd@n0B?9Y^S(TiJATb<^J}Nxp9x0 zYP-F5UHa~LK5e*1q9Er|?aX=JM>>(`_(U7*BoYpl{MHj=iBi7pG~APFmNAE#w*}M2 z-=?5u5O@3)KvY6eiAAZmM5gWAR4s{p(c!_IJ zok)vKJN(|-+K;=O%|D&NzpIusCBh8Nwe*$QUpDc=bg$ZC+Z*^U_}cd* zR^ak((1`jxI@%({{bBJP`dV3`XSl!45^v0VM*9Be}zHx?`Sk*N6!0^vfi)yICE);gON7nc9LAwx;IhiPD)kgKpUud6a)w zflEM?T74%)|QJs>~}@fy{M+0D_sFy$7K#!PtU-T15byT8sd8u=_n3>on6E zygcwgP}}>9B|xX4)1pSVfW>4nR;jZk`lX$@^on3Ja<8>=_azKn9qZ6tmETzGys}G~ z<#`9g{Y{MjM%P!;o&;rfKLB-@zhLV(x`A{$+Rzp&9M&}vENzkSa@jq#AmEBkR$z78 zhHLYuhc;F(O_iyc`E23K(v>-5LI=vwRp6G%V&{2m7i&0y4oI!Q|W|@$DBG zwSRx&t1JHEbyt7GN=`Rcr}Jwom&jPvJX`8jv^ilHQ6)fzozm}&yPY0!#aWs+XQnfE zAvjrj`tdJtL04#U?b1O!UqupZ@i@Ed7;>%)R5;NS5##`J*Z?BGO~ad$%5Oi)WO8b4 z@AncrwnR3I=XZAB^}_BSYF+C1)rv2%3{+|f#q&7Z`EMR~a_29Z9q`AyXZd=451CMa zwhpK3rk%$?6{5n#Ki8&pY36-uQJEh1dxi3Z zS^fgCVf2hZ?W%L+M!H$IjxK@9#mF}VbIEb`|IYLE-=h$AIeC{P?x|#zz(5DVPq8@q%HJbj z>UaA0MgLstO8)mzPTdS0orIA*E%1SYepgr57t_u|?m9~wRjZRnzjcWFq_BwK?*(3U zumOg$lK3|;0t@#8MOvBWuKdAoR`#oCT&P-xIRKF}CB^;DV*Ro!KV|Mo9e@tns0VTW zZf^djJOB19{|o0e(!ogZ-$nKpyZx^RO78-bx@rH2t^_?=e%rzbNA3H5KOK0~Wkk0pLN;#rqY*@;e)@D}vB@#ldWhL^ZbPR=$Z zNiQB4U0rk>_h1rHEGa258YlMs)sFtd&gSr9kB^J^8$*lyZbAJOZgrBem8*@$^F_o` zJjS{SeY+a?`xX3)Q~%$m(i)@#aS2ID(()-LWSt8-8MkSK)acW%E)Iqo2-21AnJAV} zi^*!zW&Txj`I~M0Wz1Ou3l)%^fgK>ye)1{vKwfOl2*&pTaTU1gKhk0pfSNDPh z-e0xne=y>6S1f3II*$OCrRoEEHE9-c@{Vl{8-}pP(#co&yQ@3nrQ=Sg2b2(si>k(d zpxD2CkW+$9rF=1DU%P}n&J9u*=va5a2o>CF>T#qs_U%6RMyZpz&2M737 zi;a)%0~Q>hi1)b!xcj&dozIJfRiyA)m#^hO1L>@;iei&Bjwnfh3Dk!`REU+ zAf0v}sAJ^}NgljS6_P&~mk@3u!cxVWKn~D@aNd!6P=R6~5{P5CKXQM^t2_AZukiC9 z3_j~aI`D#$qBWg|pDRV4UcWAcE*nzyZ9$dOM&59ET4}SLKcExF!z!c|N+-thgO4{u zK4krin8I(++V2(JtPfeIfFp0VYI4l5d0(f9vjY`tIwW_QyQ_7?Vpy1(!rGYi=Drk! zMwLe&hh55jE2;82(Q+4`GE(b{m}m3InP_4%Cne>YV2;As;O`&tn`P!~V0(Ic3VN*c z#&b~9r#te@$%u1lw&U$qbsGDiD+B zMO>6?YtKNKJdMbk&X>6MIt$-nnDxHny}VWV+2ZV0Wy+&|_oAjp_Sq$0r6z>x=t$h6 zm$ZqtvN6s=!sWA1%1~Fix}x^T-z`h@dRNw^X13a;9@`uFl9eAd!)xonWXuK?2+j@U z;Wl1>IXK;kX#ZTZaD{!eo^uPiRIgiRA$P>KReeaWEhkBg@#CerIg2FHdGK1=lRA3MlR7tQO)3#ZC=}Jme211cy>5yzSWmQ$x#{`^2Q@fZoKP2YMbY&cl=hO!j zX$>Pf&l5L_Ur;CqEJ2Qev2{b64;6uBwW$h1X`H zI-WaDzVtiC1IFZMwP}l`l5pO`rI4HNPu&q1&CzGgvrQ89ebM`(_5%)sje!06xubo#Op|D@nHVsWb4NSx&T1Yut? z8M7|WXiv3sn+M&Ea*8A|R;3D~ud&W6A9lr=thj~URikXyVAjSa{0}<5Dg_$j$Mnk|#ao`nIrlwUTgH2y`p_#zpJi9Y5PspYWA_va*krsjcg^i`JG zdBdr6g79J{zqO z3p$EriKVyL)qPXIsMboNEwBCJ`~MRuf#CnA0H-N_OnQ6mY8ASAj6$d}<4kW}I)zLj zY|OTXcJMl_B*pEW0M7{ZXBWOvMttMQ$a6(d{xa=reHjgO>TZdismYU(;5LkS%-~iO z)NC)LXQR`qGQ9TW(H1ozv!&va7>lv+Xr*&DCX*}2uTfA|}989hodV~z=aJPS^P4k3*^1%uX zKkofn>8BX3J$mXD4Zo0XzOViE{Qdf6sQdk&w2~#3Bz(}4F!9QjZRdQ2_I+=(WaHdS z?i&f%ppR5)qf#-gb~E}%TjPq6_ubVHX=aLn{^@4PNOt7s;xnErJEHHff(x(QWCWBr zs;8?jCMk)gOEAnF3O2B_dw`bP9#1?xk%FLF*t)u&)w?AdRWN-yz8L{1FC~{S9UM=q z(OiUWD^Nze{=t>?nYudD58S;I_deZMe=65NMRfctq75E@aS@T~2x}vg2++rm?d?@k z?!pt>BfG|eZ&@a!Q*M|q4u)6`D_~Mp^J8M}Te6%HV~OX<{3*;32IKP6$uAi3SA#0d z)_AZasKgeqJ*SKnLn|Noi+>96;3rfo86P~b`T;mU)l6s2ynieu&#z(;jpfF~nDK69 zJRTh{tq6!VTXBRkw>vHjisQ$IW>pr2O~y%1Fr=82I{)bHyWYB>ZbbTr)S2}m2jEvo zyrN(+AC>Hak(H8DFV6)js+aM0Em%_cd3S{=e(Q+;PSxdwzKR)F;>FyGiD%V$AIG~_ z#Xv_PlJWG>L|$fQ#Cuj9$J0Lrvq|pQf_!1`KM!UB&|54lv^I{KmMgFAW@kG4-sRmS zK5CFM=+nqZ9NF2;a<$$+-6rX@Ap(nQA?-sJDj~te?*-o}kO-~=!220SJ|YqnQdCx^ z6x2@q#{wUI1uGmyC>zFQdPWrTl(#@;<)$ zIWgrJGP(ivn~$FRi*w!Ww3VjsYFEF>zW+Od|L(oKyjNyRgHys(Ksh$Ow|bN!Fb?xnS|<28-d*chf~4EFlB4lyKtf@?WklzQ0Kj6%ld6&{gH( zKP}`JQ|!OE2&A>d|K<)m5_)KR?gdag?Em>}F8M#e(!yi6ampBs!}h#e@XGR!)dRzm zCuvpJ#OIVB(E?81hUpRknCyQMVj#UcyKVr)D8l0n83E>f&DnA9kMZD@o2)ODtmP)J zEWX$L@aNEu=?O!Ybc)5Zb0!C{w!j}hEn1afA}$di&;4XP7?#oG;d!6?)s96*bvn<75ro`>0zx0 zgESPX1ahgSNE(xD&p*X6oj)P$gUK#cef&O{&bpY*a?)HbX5XN4dbl<(uZ%ibu1)PO z(V*U#v40WOeb>C2_wtDLYdeEWvosWd2&@TQTX(e(V((;M@*T@btnJ_nw zXH3x)gVgy_2I;3p$x|ZBNl`u%9qq7iDZ> z#sT+YxgiI+S-RXIM?xYVJC9ohY^O?w3Fili5u^5*OiGW~Snj@(R>Xz-*uPDZj5MX# zoT*jTF+l8CK$u3s={mAuUWuL9>=sR_!X%zm!CX1q?;`KA$ zi?he?MhIbmwhUe*Eh$fW`U2!1}oe;v^m!ne8|Zkp#wU#TGASebA?#%}K~( zc+z<;%zP}(V7c_vNu4*}pNaLdF$iSTWyipP`JWsCP{wo-70Auq_mvtE`KgZ3&d~1;^D2 z#*d{dQJ;_tHtmm?~>Drmjy3cwG_ifE)fmCX8xre+O_ zz0x-(O2NE|0!ODe1=E&_Dr}}XsVg_40)s*mE+w%HHGl{$hx6iNv1DR_*tPDc?HHFr zSn`f$Iou>p*oo;uN<_q6ebMrxWhg&$mD3&{e!1ljy@^U_Sr2dt#ORSC2o#OwTRV0` z5B{*he`A9tzS5$6d`nx*weuC-9iax0EA4o_d!x-%ow8tJ;*Nmz^VkYAF@4d>PdWnm zXQMY!FU!b3je4BA#H{X@*ewVSChl%dRD?@Ph$+c?l|P=Yx2!#;3Fn+V?i{tPEl@+m zKr_5y7441HunwYCv~R=9Oj_E3%KTNRtt|p7NWrbk-JzBQ6w!lJEK3CmwBdFf7|8n_ zn8x;4d4FABMOUBxneC+)wWIMvpM41}pEMRi_OAz_Bxxthqkx*-3BMqfqZkeA%5I&= zwG2O$=uo|~*qYVH4_3u=V24S?p{|W0{1Qts$N8w<_{L^Bt(lbM3~(yfb)hq4vxZZ5 zjJgtUKiJxGwCt(BUxKYQyg7Q=H`@9b3}&ALDL;aO>)IQR%aW#ZuA@NOwJ(cagt1p; zJO*kriaMl{5))a2W4?YOLuO{I!wMLEz1{cR*&b~4zew+($X#3Mh`5D-A}msABz<}t zxDU!l8uYV}dddXai?dCPB{psH_6dIN1v;hU)b^ugGOaLh*|t8>tccrrnJ>tjT>vM4 z)Mi)S8kT1VZ_M7@UoJUX?M0oD1RWqoGeIM!R}aNr?vH*qS9WrWD6^c_n<%#<-!0Fa zyxQ=#%}1~Ujr@XmH*!QT%)zc{Us8JD+z3hSJK7h0HnY=@l0nsar>>-Pt(tc$;+H@R zv@1D>=Knyhm>di)FBIcay!K0eIiBzgC{D-klFh19$vE0er0)UreR*k_OPm?pVe?h>F1mx& zAj&jfuu2i0zSR-i;E?%|Qe+^J94dXmrUH;%D`Zbb>{938!ktpSM4^~%36~&&Yw?Ei zM;j6h5X=g7iG?=9w=O9#&V#}PhtEE9%LI4EyuuKjJWi5n|sckOI` ztr7hs1p@S^Kpuu&jRH|Q&q*UE>R~}AG0P|ClXjzJpT!SXwiAzMSN1Bi!Ta58zM|H} z;`_(+wb0k%eMp~bkt3vA!q%A-J;t!phZYHQ-tHVxTOMC?MT|t4v!a+5kv}N=>ks_v z0Mk90t(z@a!Ssoi2g45CzetQ9wa_0fEV@HH;ZfLnR^}S{F=htC$8X#TDPhvgL`O~q zu72NQs9_33A5J^?>^mjNaLM5OPj(T<@O#rWW#y#}J3kZ_zj=a@lk-c6M;N`@R@yP0qbKeZethmt2K`>j-LOI*t(Ag=M0~n_Fa0)e zi_jN;#W1PE1dbOa6G8&Q!bT4E$aM+Wdb~b})r|^*o2;{C4lr!*jPDmz2SIEV8E%*9wEO2 zb7#KuaHe=nbD)P$ez4aAPR<{=W7l?0`x9!P11D8@w>-$8uj~-%YS=PFqNoVKlD~R$ zD_^h+)_{5e9Hw)1HWp(R*|}-GGe8e==uu|zU`!y-mpc&^;2Y|Pob_YNh)<{~FNzy2 zTM9Bl5+$8Ki|-W7P+a-$=QY@L9sbeP{`s_|a{TwZmNCmK6L{i3|LZEBhc{>$RN8aLhE^`VkI} z^+@NHwXE@6KU9$U!vK#V74q9Kw~Z;*M&xkmmw0fQ<%6dj1x(xeP@p2eVS(R6h*~hETq#tL+XlLD=22FAwb%(bhPWS~E zg&-$k(AIPrretVESp22ZhI%oInllu%&L#3wyD6u3olUYxy<2c>S9igqnU?9= z3g`3^^BEa+JFeSP4{V+=2EeHp5QuJOp$^O>|{&v=Edfe(-}DB={|6`OsL2B*6(X3^i{h(hA#!lZB!7WJFv} zstC~DO;L>3#tyfqh9X$(n5aR4@FdnsdWxk3c_1{!akJrY z;(l+UIQQ`}_7)q26XM6NX0hN~qDfIQ#R(J>V*$h&%Uv8SC?e(1w&Hd`pw#+1~59gpvXZ3CM1*3XW zWvlS%kSM$P!y1R~@ov18-t;04!NBC$mQi@o7LP%yvB1|41JM{*^F7h0PKlHCTqA?U zb}0{{gD`>3xQJap?VhhZ&~6P_(u};{#xje1U+vaX=<(TlpD= z`*^i(&gsm~h2ugeBk`cJMK$9+PMpapei)J_%osg?d(j=F$!GEQ*w4hcqMw=SkoBR| z#i`^~-2E;xa`{wyNt_46jZ+;&j4RH{{4}dWn9%&*t})TEZUG8+Qo$Oh0WdObaJ14cA|hL8qvpHJRQw znG1R{Blfz3;snNuXdG-NB>OzB03GPjlPPvK#ws;1A~q`Mfn(jwRbEMID*XDnMXc4S z4y@58P3N0lekUTYH2EgaEwoJr(>Bp0?Zb58P+EFubbMZOwCo#*uLJ=H@D{uwk2f&t zp!TAUG5fNoK1(}TtWPnuOC6snJms;t=Cx?p-Ltch$?kQ}2#v@*TS7*OEcE!rSb*lq zvsO%yR(Yq?&_$UU50}Qb2wT3fokCFK_OcZb%f4k}uGxMvJ#%6~x6O4^yVJkIOoMAO z>GcR^~GyI@=M^ zi=?_|E>>;02b{0JidsV57Q5I-hBdhL@_j)Xgr_bw)~BczO3k`B+K!-({NLsU{*wo3 zr4K4i3u9*8F`VnU@=mty1aG(;4Q|vz*uiey87AM;e(jw3@fdg$OEo3BY({->KcYd`^WMrZnE~92G9u3Us=x4h3G2i^ef?M;V#I29L>>|2mw{gOQ z|B9aaa9%mz=Hp;Ie?m0R*HTLn<*&y;>$2i=>--KXCPT27ZCb`PI5ckvL^0Gm`Kw-f z3LM<$*eGlnzavg>a_n$Yakko%9cx4zTd;Uk#CtYA25(g-iw=2KC0Um2*4r0`_QBBf zWxh(8pA0ap5w1kiHAm#$vYqBBKYsHDX?F+OfH5^SmaS@qTLo?GfKqYP&{&0E0$j}>}o}9G$cC4MagZQ`SYo>x2b|>w-Y}9cI z59YSUL4-$m=6!PZFyj{$+bJWt8jeq~Tq?Wtv{y&6J#TR69|%WvwTY@~eI1ew*#Wjz zJYVyISsjdW6~8D({bhDyi(JxEozdcBC~a1ayOV4@_PJZISJ4ffHayA7>H5Re_Jh`R z*S!{Gb!+j6K8DvDqXxoiLZUhv_V(LFxAItU{!Ma~@C;J{ArJ>}y84KAA=3QGdN+6^ zN7d^K52(-UFOLYDZFcPo+c2SabC`yFedCx57PlIFp+{pV5IlXy{}6+!uPvoEu3`4_bm zIeHRFuAnVrpKWkc5>?3NNv|EeUOwjoW`8&Q31QGDcQwLwQ|;=>Fg?>XJlX->Dy}af zyyYZVU%Mq_cHCSW%0r_f(@I+M3mqKE3p!^POwGx=mX1dpY+VTx`YMeD9mgx<*<>u; z+{n8K<*PgN%X%c(Hg>O4i0xK}8;poHxt%^YLc=}t6O+2>!YH&#W~}B@eym>c=OwVM6NC0UBPIbO6 zA&RPssSLl$tZ7%ZC;Rnw?b(dn1u<^Tnl~`!h1oa<1X~52t5#vVnmEo!_We-D$yFw% z2SkDPoH}AO<%?a_^C>(3+9?<=lF)k%Zj?gku1Zivd)|_zqa*!Ny1@;H;WHH&5iLPF zdaCco>haW5vvro*;^^GJ#vu*AxxlzhDw^Pa9SRLdyXcGkz}U$gBL;LjLoqW&Pw(TE zWEO}S?~ul0F|@-i((2Km9jE;_99e<}^MPRz@1idBd+C{72cHr7%}kXL4)jUtynyK~ zq2%`+Erl;x-9J5RVzKWh?_s!aDG^6eu*kR*XSk85D(JS?_5yyQ?V^d%)<2`I{JM@t z8ujW|R&uGf=H!%=#vk;&mtNFgXa2K1qYIZM=CC~`9;d>z)Ki5cYv^G$ZGBQ+RK!8}|*9s?d*_CoH zx$k|pHBwcbaI-|lf0ba-4xWG>6irX&!m7+lA81PW;Gl6{TC(g*B0SdH0vxLAl$Exn z$!p=NX=D|_j&;hARpuwk#b1Zw3SC&b5W>G1W~l9=tsygXLe>GDq2}z+kLi_rS9k4@ z&TL3OkjYaCr2V4$8NLqMzYxsV6;vQ%WX*{jDdMB7YrdHC#sm53C4^NgnGn8?DAv7{ z@BslLd8JhGRQ42?Zl#Kru0=+ijt6&3u|T~!V0`1zBuEK&RL7_AmhoJ-IeTtN%htp7 z$uH5`ArN@}(FR$3=o3t(VNIB3rWeScHCJ2NIC!LfQsv@6nlyrKtvGBx=uWZ3{nI+I z63f;lE?ziyhJ_1>4}KKL8OhfZBIou;Bda|7!obb%Ql`?x7X86AGNm0_oX^Qu((#$p z8*805^!&Kol`LzjRFYM<)$4TVREDKXe~^$T*BgLd%o@JX?9gQTt4LPmn=d`zKyD>J zf%P&Ft=v0_%2!Sl-V2RlW0_wuoGR8Vd_LSnjXBhih$gI-R9iS&mMcZd4_ z+L26GEnwX+)$sMu3gCnDt@o*O>hyKJTcW%8)#j((Q@B>1$1`bAs5uF#uK~Vzh3nB= zK?B8{{*%wDjvDGl%pVw7sZunEz^@+>1x631DB5s51pZ%DZxB64_x9C=m!BZ0jPFIj z8$HKXR~q71%ZKpNFnB zMKbVeV@6gucYO)kF%qUjIG6aNzqK(x|IDTp$;BZ_CLIlbE3OPXq&|+=p{912{_whv z3gAM*UE0GWPWK7~49ZwTam77dFigE~lI;e(5YI0%yUjEC75b9vUk{50M2w_4k-+ZM zyuBI1ZVEAod$_(e8+^4=h?QKRK+C94f^))!qh|_WP|POpFz_e3vJTIXbyZTRb+%1q zMEgBCFSc27Q@0Clkj_H8f-I(EY%unB8EucyZ+yRrTu*b60xCU^G)A|F~BW00< zXE(O^A7CkPFSmuwEwY(wOks)9hvq$mvV+eth;Gb5d;CbdcrIYeru9WW5&l-5A~|VG_(hjOBO&D1K(Rneo`|I}mQ2qL;ZcLg8!QOYA$C0-Fq3Iutj@XeH=t;7G82d> zcVDiKelVdI++)AGM<8#qsV|R5S84cV2bee`8he_>CyG2>?reoj@8&8T8g`TMjWo^FJu`Qs!*#cD8?&V(`>|fNh zG;#+aiFm%G4ydVBPsu_fWY40h^WHmPU(%|e^gb+zKb!Q+MLKH-XNRND_N?>$7aAtK zxKPQ$0*3!K@w6kFJp>kem4N$$6IWrF(gYXV!g5|{@mSc(?tD2tQ-0l{>kgsg(KYwG zRkz(v>uHZWrO8S-l!|NzAfDd(jq6nQ2xs{v(NGc6FewuCkp<>x4vdlc_;?ppAUj|b zhvpQlRnPY7$UGNqdM3Olybjctd@V%8^;hDH^AfK`UP%r^7B`;m7*O^L8Jdi8oLYMidp$AU>Uw3$kY!d_YI3k{QQ z?kQj+H7PpiK@ZKYna41YB)6MVG(Z#7N+@nIq^$KYE5&jZ9w$Rh<*7fmGex~# z`FQN-tP4*KFSK=Egf28P zRTInoDvaJsM3P3mYw2LxYH~PP+i8!*=XApojXa$(JYYKIWFq1oM=7wyjMtbiMcXox z@dl=sbcpTOM&w|}Gx%^;BNNdYJToY0>`hT*=^?)}}&c0)~ zv19NN(o3ui)U37yk8;sM+8;mFXj8T+Y)BGr9*jyRW7h6qhXp&~LaY?j3Gcfi?B{ z`wTlYO#<~c5ZRyIXEtVoJ?8_WnShlW%mLL%XeWufFJ{)-Xj!)HBP6wVY*^2&RTm1o zJlyhJ@$MFDrTm+|OW+t?VYF9G-W6!bEi_?_&}3#X5dszUW*H0ST-=>1XWeaycR#Zt zKk=a*n~Q>0t>`c-T^7aR&Dhls26n}@%Z{;K_l2Wd>iySrDdp)d>8qE_R|XEG158w| z8gm`z)9^Fk;5_%Mnh>3;F^BlkJel4cIlQlWA?6C61_F;~`v z1?P)H)Upflp~m)|{hV6s8OBt(q6iyV`5OaMw1nc!wT( zoJGVt{s6+BF6iC<6(V-?i0m=@F~+EOOFmQX>a5G+1;NMBXxn~7EW<90E<=E5x!Qo_ zq|vj)QNO}A4hkaXD=ksg-exKJTmxpiwOxTd^>UI09Mx`aUe)`~iAOCi+j-~!9=bKD zJ8`y!8LJJIylNvAbteFZt2}Gka$&f|SDTp8ixyC`g8oAE8OB=_Wb{whpDjxw(SG*D zqTQU$$jtOb9Xhl1tw4m=0s-H#|?+J;F*zz zBVNh!_QM%Wo@APA$jYkHQcS!tI{+t%nGc6R)}OwZ1W zMl3rO<(lGxBS9ugpK7nT_u=n=o}@rk3g_+m+3BpF0@UW>D&5OZ6VFj4Y>WE`&sWRk zw1+E9WJxFL-Vk9)6WgR4I))xio%>os%$a z9EdU>GZ_wKbQ3H-FVvoz2(KxXv8X;5>x1p<_CwJb9zN`K^S(BEW0cw4syyKwTeQzD zN71sQpeSaQXJ1{ClO zrb4Ur85)w2i`4E68&>hd|INd8A0YdnqtC9)P`E*!A`abQY7B3qYU`XraN066hKa}e z6jYozj|-v*1YM0MVv-EN%Y7AwHtfa`)vLpbwP9)AsUG3$G-E zEA78}n5EmfyxGr|p11|vy=2Iz3kAlzRa{jC6x)XkCPu0*?y<){*U7Pm1rUq(1`bY4 zNHRw9!CEM%K6g|m88pcQ37G7ifeM>z{OnIu{%vfK)qP;RP|UI(ivr3mD;LO=2Df

QPF7&Ie;E`5;HK&Xr^?h}Mr^uf59UYPynwt9# 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 literal 0 HcmV?d00001 From 5f25fa1227de9bb909a0681eb719fe07f8c098cc Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Dec 2023 15:58:30 -0500 Subject: [PATCH 02/96] Tweak wording --- CONTRIBUTING.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fbb2aa29488f5b62adcd35fbe3a25c54614ea894..110120c061603739c5f7bd9d7fdf783346fcef5a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,19 +2,21 @@ ## Introduction -[Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every week, aggressively dogfooding our own tools. +[Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every day of the workweek, aggressively dogfooding everything. -![Staff usage of channels (metrics were not being collected before August, 2023)](./assets/screenshots/staff_usage_of_channels.png) +![Staff usage of channels](./assets/screenshots/staff_usage_of_channels.png) -While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves, but we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. +*Metrics were not being collected before August 2023* + +While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. ## Contribution ideas -*If you already have an idea of what you'd like to contribute, you can skip this section.* +If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: -- Our public roadmap [include link] shows the largest, most-wanted features we plan to add to Zed. +- Our public roadmap [include link] details what features we plan to add to Zed. - Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. *If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* From c7d60bb003fa8ff1dc829778ba9b88ad408db122 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 14 Dec 2023 16:10:53 -0500 Subject: [PATCH 03/96] Add some TODOs --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 110120c061603739c5f7bd9d7fdf783346fcef5a..42afe02fd2663f72c95d8d601f51cab6e046c367 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,9 @@ Reviewing code in a pull request, after the fact, is hard and tedious - the team Other things to mention here - [ ] Etiquette - [ ] CLA +- [ ] Importance of tests +- [ ] Look over Piotr's PR and pull in what this is missing (tour of the codebase, etc.) + - https://github.com/zed-industries/zed/pull/3143/files Things to do: - [ ] Put names devs who "own" each channel in the channel notes From 2fd9ac506fa24cf6664d14009142047847b5b70b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 15 Dec 2023 01:47:59 -0500 Subject: [PATCH 04/96] Tweak wording --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 42afe02fd2663f72c95d8d601f51cab6e046c367..09e9f3d1d16e1657485a4ed5aa6b0e3da0aeb55b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ ![Staff usage of channels](./assets/screenshots/staff_usage_of_channels.png) -*Metrics were not being collected before August 2023* +*Channel metrics were not collected prior to August 2023* While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. From 6cb1a08cc9fe8d969e6ca0b2a5f44f3934b43f51 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 15 Dec 2023 17:18:52 -0500 Subject: [PATCH 05/96] Add notes --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 09e9f3d1d16e1657485a4ed5aa6b0e3da0aeb55b..64fb8246517d3ac3f64363263497bc5c95726117 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,8 @@ Other things to mention here - [ ] Importance of tests - [ ] Look over Piotr's PR and pull in what this is missing (tour of the codebase, etc.) - https://github.com/zed-industries/zed/pull/3143/files +- [ ] Ask people to check the PRs to see if something has already been started on +- [ ] Maybe have a channel that maps out what teammates / community users are working on what, so people can see what's being worked on Things to do: - [ ] Put names devs who "own" each channel in the channel notes From 3c2db6a4db646e53f20df2b648bf49e8819bcfc4 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 18 Dec 2023 13:35:07 -0500 Subject: [PATCH 06/96] Add some rough ideas Co-Authored-By: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- CONTRIBUTING.md | 87 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 73 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64fb8246517d3ac3f64363263497bc5c95726117..bde030de4746c42c8d4667e9729c455f7127dbc0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,59 @@ # CONTRIBUTING -## Introduction +Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor! + +## Contribution ideas + +If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: + +- Our public roadmap [include link] details what features we plan to add to Zed. +- Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. + +*If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* + +In the short term, we want to provide a generalized solutions to these problems (plugin system/theme system), so we are not looking to add these features to Zed itself. + +- Adding languages +- Themes + +## Resources + +### Bird-eye's view of Zed + +Zed is made up of several smaller crates - let's go over those you're most likely to interact with: +- [gpui](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. +- [editor](/crates/editor) contains the core `Editor` type that drives both the code editor and all various input fields within Zed. It also handles a display layer for LSP features such as Inlay Hints or code completions. +- [project](/crates/project) manages files and navigation within the filetree. It is also Zed's side of communication with LSP. +- [workspace](/crates/workspace) handles local state serialization and groups projects together. +- [vim](/crates/vim) is a thin implementation of Vim workflow over `editor`. +- [lsp](/crates/lsp) handles communication with external LSP server. +- [language](/crates/language) drives `editor`'s understanding of language - from providing a list of symbols to the syntax map. +- [collab](/crates/collab) is the collaboration server itself, driving the collaboration features such as project sharing. +- [rpc](/crates/rpc) defines messages to be exchanged with collaboration server. + +// Let's try to make whoever we come into contact with for the first time +// well-equiped to discuss basic concepts around Zed +// Ideally these should link to mdbook/source code docs (doubtful given how source code docs might be more in-depth than necessary) +### Important concepts + +- Views vs Models +- Contexts +- Action +- UI + - Render vs RenderOnce + - ui crate + - storybook +- Workspace +- Project + - Worktree +- vim crate + - Editor + - Multibuffers +- Settings + +## Zed channels + +Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. [Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every day of the workweek, aggressively dogfooding everything. @@ -10,37 +63,43 @@ While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. -If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. +### Proposal & Discussion -## Contribution ideas +To do that, find a public channel [link to list of all public channels] that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. *Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* -If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: +## Implementation & Help -- Our public roadmap [include link] details what features we plan to add to Zed. -- Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. +Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. -*If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* +Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. -## Proposal & Discussion +--- Piotr's original contribution guide --- -Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. Find a public channel [link to list of all public channels] that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. *Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* +Read on if you're looking for an outline of your first contribution - from finding your way around the codebase and asking questions, through modifying and testing the changes, finishing off with submitting your changes for review and interacting with Zed core team and Zed community as a whole. -## Implementation & Help +### Getting in touch +We believe that journeys are best when shared - hence there are multiple outlets for Zed users and developers to share their success stories and hurdles. -Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. +If you have questions, ask them away on our [Discord](https://discord.gg/XTtXmZYEpN) or in a dedicated [Zed channel](https://zed.dev/preview/channel/open-source-81). We also plan to organise office hours on a weekly basis - they will take place in forelinked Zed channel. -Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. +All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). ---- +If you're just starting out with Zed, it might be worthwhile to look at some of the other crates that implement bits of UI - such as [go to line](/crates/go_to_line) modal that's bound to ctrl-g by default in Zed. + +### Upstreaming your changes +Here be dragons :) +--- Other things to mention here - [ ] Etiquette - [ ] CLA - [ ] Importance of tests - [ ] Look over Piotr's PR and pull in what this is missing (tour of the codebase, etc.) - - https://github.com/zed-industries/zed/pull/3143/files + - See above - [ ] Ask people to check the PRs to see if something has already been started on - [ ] Maybe have a channel that maps out what teammates / community users are working on what, so people can see what's being worked on +- [ ] Mention Discord or keep it only focused on Zed channels? +- [ ] Mention issue triage doc (https://github.com/zed-industries/community/blob/main/processes/issues_triage.md)? Things to do: - [ ] Put names devs who "own" each channel in the channel notes From 364e33df82472fbb4678b70afde705495db61b4d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 8 Jan 2024 13:25:52 -0500 Subject: [PATCH 07/96] Add period --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bde030de4746c42c8d4667e9729c455f7127dbc0..41261250f32005ef669c8b3e4ba830f4fa96a19c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,7 +11,7 @@ If you already have an idea of what you'd like to contribute, you can skip this *If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* -In the short term, we want to provide a generalized solutions to these problems (plugin system/theme system), so we are not looking to add these features to Zed itself. +In the short term, we want to provide a generalized solutions to these problems (plugin system/theme system), so we are not looking to add these features to Zed itself - Adding languages - Themes From 5f1513893398b4b0ed084feb0f32dfeefe3b2e08 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 9 Jan 2024 13:41:42 -0500 Subject: [PATCH 08/96] WIP Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- CONTRIBUTING.md | 79 +++++++++++-------------------------------------- 1 file changed, 17 insertions(+), 62 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 41261250f32005ef669c8b3e4ba830f4fa96a19c..825ddce79d2611bf1e48f4e2fcc11bdc98beeb6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,26 +2,26 @@ Thanks for your interest in contributing to Zed, the collaborative platform that is also a code editor! +We want to ensure that no one ends up spending time on a pull request that may not be accepted, so we ask that you discuss your ideas with the team and community before starting on a contribution. + +All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Contributors to Zed must sign our [Contributor License Agreement TODO](LINK) before their contributions can be merged. + ## Contribution ideas If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: -- Our public roadmap [include link] details what features we plan to add to Zed. +- Our [public roadmap TODO](LINK) details what features we plan to add to Zed. - Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. -*If you are a plugin developer looking to contribute by building out the Zed ecosystem, have a look at these [issues](https://github.com/zed-industries/community/issues?q=is%3Aopen+is%3Aissue+label%3A%22potential+plugin%22+sort%3Areactions-%2B1-desc).* - -In the short term, we want to provide a generalized solutions to these problems (plugin system/theme system), so we are not looking to add these features to Zed itself - -- Adding languages -- Themes +At the moment, we are generally not looking to extend Zed's language or theme support by directly adding these features to Zed - we really want to build a plugin system to handle making the editor extensible going forward. This isn't to say that we won't accept contributions that add support for a new language or theme, but more to emphasize that we want to discuss these types of contributions first. ## Resources ### Bird-eye's view of Zed Zed is made up of several smaller crates - let's go over those you're most likely to interact with: -- [gpui](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. + +- [gpui](/crates/gpui) is a GPU-accelerated UI framework which provides all of the building blocks for Zed. **We recommend familiarizing yourself with the root level GPUI documentation** - [editor](/crates/editor) contains the core `Editor` type that drives both the code editor and all various input fields within Zed. It also handles a display layer for LSP features such as Inlay Hints or code completions. - [project](/crates/project) manages files and navigation within the filetree. It is also Zed's side of communication with LSP. - [workspace](/crates/workspace) handles local state serialization and groups projects together. @@ -31,29 +31,9 @@ Zed is made up of several smaller crates - let's go over those you're most likel - [collab](/crates/collab) is the collaboration server itself, driving the collaboration features such as project sharing. - [rpc](/crates/rpc) defines messages to be exchanged with collaboration server. -// Let's try to make whoever we come into contact with for the first time -// well-equiped to discuss basic concepts around Zed -// Ideally these should link to mdbook/source code docs (doubtful given how source code docs might be more in-depth than necessary) -### Important concepts - -- Views vs Models -- Contexts -- Action -- UI - - Render vs RenderOnce - - ui crate - - storybook -- Workspace -- Project - - Worktree -- vim crate - - Editor - - Multibuffers -- Settings - ## Zed channels -Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. If you're new to Zed's channels, here's a guide [link to up-to-date docs] to help bring you up to speed. +Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. If you're new to Zed's channels, here's a guide [link to up-to-date docs TODO](LINK) to help bring you up to speed. [Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every day of the workweek, aggressively dogfooding everything. @@ -63,43 +43,18 @@ Once you have an idea of what you'd like to contribute, you'll want to communica While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. -### Proposal & Discussion - -To do that, find a public channel [link to list of all public channels] that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. *Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* - -## Implementation & Help - -Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. - -Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. - ---- Piotr's original contribution guide --- +We plan to organize office hours on a weekly basis - they will take place in forelinked Zed channel. -Read on if you're looking for an outline of your first contribution - from finding your way around the codebase and asking questions, through modifying and testing the changes, finishing off with submitting your changes for review and interacting with Zed core team and Zed community as a whole. - -### Getting in touch -We believe that journeys are best when shared - hence there are multiple outlets for Zed users and developers to share their success stories and hurdles. +### Proposal & Discussion -If you have questions, ask them away on our [Discord](https://discord.gg/XTtXmZYEpN) or in a dedicated [Zed channel](https://zed.dev/preview/channel/open-source-81). We also plan to organise office hours on a weekly basis - they will take place in forelinked Zed channel. +Before starting on a contribution, we ask that you look to see if there is any existing PRs, or in-Zed discussions about the thing you want to implement. If there is no existing work, find a [public channel TODO](LINK) that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. -All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). +*Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* +## Implementation & Help -If you're just starting out with Zed, it might be worthwhile to look at some of the other crates that implement bits of UI - such as [go to line](/crates/go_to_line) modal that's bound to ctrl-g by default in Zed. +Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. -### Upstreaming your changes -Here be dragons :) ---- -Other things to mention here -- [ ] Etiquette -- [ ] CLA -- [ ] Importance of tests -- [ ] Look over Piotr's PR and pull in what this is missing (tour of the codebase, etc.) - - See above -- [ ] Ask people to check the PRs to see if something has already been started on -- [ ] Maybe have a channel that maps out what teammates / community users are working on what, so people can see what's being worked on -- [ ] Mention Discord or keep it only focused on Zed channels? -- [ ] Mention issue triage doc (https://github.com/zed-industries/community/blob/main/processes/issues_triage.md)? +**Zed makes heavy use of unit and integration testing, we encourage you to write tests for your contribution.** -Things to do: -- [ ] Put names devs who "own" each channel in the channel notes +Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. From 5885f03f35b12c9ffb1a9e799e2023f29bc9f654 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 11 Jan 2024 11:24:17 -0800 Subject: [PATCH 09/96] Add migration information to release docs and fix scripts --- docs/old/release-process.md | 14 +++++++++----- script/deploy-collab | 2 +- script/what-is-deployed | 7 +------ 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/old/release-process.md b/docs/old/release-process.md index ce43d647bd723d1343d00d3f1c571c29e2df6092..6162304a7b0a74fe3bfad2cc110d9fbc6a820c51 100644 --- a/docs/old/release-process.md +++ b/docs/old/release-process.md @@ -87,10 +87,14 @@ This means that when releasing a new version of Zed that has changes to the RPC 1. This script will make local changes only, and print out a shell command that you can use to push the branch and tag. 1. Pushing the new tag will trigger a CI build that, when finished will upload a new versioned docker image to the DigitalOcean docker registry. -1. Once that CI job completes, you will be able to run the following command to deploy that docker image. The script takes two arguments: an environment (`production`, `preview`, or `staging`), and a version number (e.g. `0.10.1`). +1. If needing a migration: + - First check that the migration is valid. The database serves both preview and stable simultaneously, so new columns need to have defaults and old tables or columns can't be dropped. + - Then use `script/deploy-migration` (production, staging, preview, nightly). ex: `script/deploy-migration preview 0.19.0` + - If there is an 'Error: container is waiting to start', you can review logs manually with: `kubectl --namespace logs ` to make sure the mgiration ran successfully. +1. Once that CI job completes, you will be able to run the following command to deploy that docker image. The script takes two arguments: an environment (`production`, `preview`, or `staging`), and a version number (e.g. `0.10.1`): - ``` - script/deploy preview 0.10.1 - ``` +``` +script/deploy preview 0.10.1 +``` -1. This command should complete quickly, updating the given environment to use the given version number of the `collab` server. +1. This command should complete quickly, updating the given environment to use the given version number of the `collab` server. \ No newline at end of file diff --git a/script/deploy-collab b/script/deploy-collab index c5386298fa2d5fc8f0357c0d3e96601bf786866e..8dbee18e3b78cfb1a26a74338509a69db54b0307 100755 --- a/script/deploy-collab +++ b/script/deploy-collab @@ -4,7 +4,7 @@ set -eu source script/lib/deploy-helpers.sh if [[ $# < 2 ]]; then - echo "Usage: $0 (nightly is not yet supported)" + echo "Usage: $0 " exit 1 fi environment=$1 diff --git a/script/what-is-deployed b/script/what-is-deployed index b6a68dd3b3245bdf925ffe2d80c23725e43c1c81..c0f9b234878527da913ba320ffcca89d094b2c32 100755 --- a/script/what-is-deployed +++ b/script/what-is-deployed @@ -4,16 +4,11 @@ set -eu source script/lib/deploy-helpers.sh if [[ $# < 1 ]]; then - echo "Usage: $0 (nightly is not yet supported)" + echo "Usage: $0 " exit 1 fi environment=$1 -if [[ ${environment} == "nightly" ]]; then - echo "nightly is not yet supported" - exit 1 -fi - export_vars_for_environment ${environment} target_zed_kube_cluster From 8c9f3a7322ee2be43859e6cc7976e08433382c54 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 15 Jan 2024 17:01:07 -0500 Subject: [PATCH 10/96] init color crate --- Cargo.lock | 76 +++++++++++++++++++++++- crates/color/Cargo.toml | 32 +++++++++++ crates/color/src/color.rs | 118 ++++++++++++++++++++++++++++++++++++++ crates/zed/Cargo.toml | 1 + 4 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 crates/color/Cargo.toml create mode 100644 crates/color/src/color.rs diff --git a/Cargo.lock b/Cargo.lock index 056fab49cd576915fca5443922b0f094591237e1..1b0c6e4bb97ebc9fb8ce9615729fb370a5b18a34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1580,6 +1580,28 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "color" +version = "0.1.0" +dependencies = [ + "anyhow", + "fs", + "indexmap 1.9.3", + "itertools 0.11.0", + "palette", + "parking_lot 0.11.2", + "refineable", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings", + "story", + "toml 0.5.11", + "util", + "uuid 1.4.1", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -4976,6 +4998,7 @@ dependencies = [ "approx", "fast-srgb8", "palette_derive", + "phf", ] [[package]] @@ -5164,6 +5187,48 @@ dependencies = [ "indexmap 2.0.0", ] +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher 0.3.11", +] + [[package]] name = "picker" version = "0.1.0" @@ -7073,6 +7138,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "slab" version = "0.4.9" @@ -7643,7 +7714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" dependencies = [ "float-cmp", - "siphasher", + "siphasher 0.2.3", ] [[package]] @@ -8857,7 +8928,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher", + "siphasher 0.2.3", "svgtypes", "ttf-parser 0.12.3", "unicode-bidi", @@ -9649,6 +9720,7 @@ dependencies = [ "client", "collab_ui", "collections", + "color", "command_palette", "copilot", "copilot_ui", diff --git a/crates/color/Cargo.toml b/crates/color/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c6416f9691b3ca417c1f7426ace5359c199be88b --- /dev/null +++ b/crates/color/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "color" +version = "0.1.0" +edition = "2021" +publish = false + +[features] +default = [] +stories = ["dep:itertools", "dep:story"] + +[lib] +path = "src/color.rs" +doctest = true + +[dependencies] +# TODO: Clean up dependencies +anyhow.workspace = true +fs = { path = "../fs" } +indexmap = "1.6.2" +parking_lot.workspace = true +refineable.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +settings = { path = "../settings" } +story = { path = "../story", optional = true } +toml.workspace = true +uuid.workspace = true +util = { path = "../util" } +itertools = { version = "0.11.0", optional = true } +palette = "0.7.3" diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs new file mode 100644 index 0000000000000000000000000000000000000000..77818eb7b8f76c23cc9b05e31402ad04a89aeca3 --- /dev/null +++ b/crates/color/src/color.rs @@ -0,0 +1,118 @@ +//! # Color +//! +//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality. +//! +//! It is used to create a manipulate colors when building themes. +//! +//! **Note:** This crate does not depend on `gpui`, so it does not provide any +//! interfaces for converting to `gpui` style colors. + +use palette::{FromColor, Hsl, Hsla, Mix, Srgba, WithAlpha}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum BlendMode { + Multiply, + Screen, + Overlay, + Darken, + Lighten, + Dodge, + Burn, + HardLight, + SoftLight, + Difference, + Exclusion, +} + +/// Creates a new [`palette::Hsl`] color. +pub fn hsl(h: f32, s: f32, l: f32) -> Hsl { + Hsl::new_srgb(h, s, l) +} + +/// Converts a hexadecimal color string to a `palette::Hsla` color. +/// +/// This function supports the following hex formats: +/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`. +pub fn hex_to_hsla(s: &str) -> Result { + let hex = s.trim_start_matches('#'); + + // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA + let hex = match hex.len() { + 3 => hex + .chars() + .map(|c| c.to_string().repeat(2)) + .collect::(), + 4 => { + let (rgb, alpha) = hex.split_at(3); + let rgb = rgb + .chars() + .map(|c| c.to_string().repeat(2)) + .collect::(); + let alpha = alpha.chars().next().unwrap().to_string().repeat(2); + format!("{}{}", rgb, alpha) + } + 6 => format!("{}ff", hex), // Add alpha if missing + 8 => hex.to_string(), // Already in full format + _ => return Err("Invalid hexadecimal string length".to_string()), + }; + + let hex_val = + u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?; + + let r = ((hex_val >> 24) & 0xFF) as f32 / 255.0; + let g = ((hex_val >> 16) & 0xFF) as f32 / 255.0; + let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0; + let a = (hex_val & 0xFF) as f32 / 255.0; + + let srgba = Srgba::new(r, g, b, a); + let hsl = Hsl::from_color(srgba); + let hsla = Hsla::from(hsl).with_alpha(a); + + Ok(hsla) +} + +/// Mixes two [`palette::Hsl`] colors at the given `mix_ratio`. +pub fn hsl_mix(hsla_1: Hsl, hsla_2: Hsl, mix_ratio: f32) -> Hsl { + hsla_1.mix(hsla_2, mix_ratio).into() +} + +/// Represents a color +/// An interstitial state used to provide a consistent API for colors +/// with additional functionality like color mixing, blending, etc. +/// +/// Does not return [gpui] colors as the `color` crate does not +/// depend on [gpui]. +#[derive(Debug, Copy, Clone)] +pub struct Color { + value: Hsla, +} + +impl Color { + /// Creates a new [`Color`] + pub fn new(hue: f32, saturation: f32, lightness: f32) -> Self { + let hsl = hsl(hue, saturation, lightness); + + Self { value: hsl.into() } + } + + /// Creates a new [`Color`] with an alpha value. + pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { + Self { + value: Hsla::new(hue, saturation, lightness, alpha), + } + } + + /// Returns the [`palette::Hsla`] value of this color. + pub fn value(&self) -> Hsla { + self.value + } + + /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`. + pub fn mix(&self, other: Hsl, mix_ratio: f32) -> Self { + let mixed = self.value.mix(other.into(), mix_ratio); + + Self { + value: mixed.into(), + } + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 734c225cb1e610c64dab92112accad9634632fee..1e21648408e4855e74cf4f411bc287345bac2bee 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -29,6 +29,7 @@ command_palette = { path = "../command_palette" } # component_test = { path = "../component_test" } client = { path = "../client" } # clock = { path = "../clock" } +color = { path = "../color" } copilot = { path = "../copilot" } copilot_ui = { path = "../copilot_ui" } diagnostics = { path = "../diagnostics" } From bdb06f183b09a15340c9a83221432d54fc10a41a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 16 Jan 2024 00:07:06 -0500 Subject: [PATCH 11/96] Add a rudimentary state color builder --- Cargo.lock | 1 + crates/color/src/color.rs | 57 +++++++++++++++++++++++++++++++++------ crates/theme/Cargo.toml | 1 + crates/theme/src/theme.rs | 7 +++++ 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b0c6e4bb97ebc9fb8ce9615729fb370a5b18a34..728fc695ac0891bb20f06a70d6c35ee2dfe65876 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7925,6 +7925,7 @@ name = "theme" version = "0.1.0" dependencies = [ "anyhow", + "color", "fs", "gpui", "indexmap 1.9.3", diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs index 77818eb7b8f76c23cc9b05e31402ad04a89aeca3..8a7f3f17525cd097dfb4072c43b83675f54323c3 100644 --- a/crates/color/src/color.rs +++ b/crates/color/src/color.rs @@ -88,25 +88,28 @@ pub struct Color { } impl Color { - /// Creates a new [`Color`] - pub fn new(hue: f32, saturation: f32, lightness: f32) -> Self { - let hsl = hsl(hue, saturation, lightness); - - Self { value: hsl.into() } - } - /// Creates a new [`Color`] with an alpha value. - pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { + pub fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { Self { value: Hsla::new(hue, saturation, lightness, alpha), } } + /// Creates a new [`Color`] with an alpha value of `1.0`. + pub fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self { + Self::new(hue, saturation, lightness, 1.0) + } + /// Returns the [`palette::Hsla`] value of this color. pub fn value(&self) -> Hsla { self.value } + /// Returns a set of states for this color. + pub fn states(&self, is_light: bool) -> ColorStates { + states_for_color(*self, is_light) + } + /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`. pub fn mix(&self, other: Hsl, mix_ratio: f32) -> Self { let mixed = self.value.mix(other.into(), mix_ratio); @@ -116,3 +119,41 @@ impl Color { } } } + +/// A set of colors for different states of an element. +#[derive(Debug, Copy, Clone)] +pub struct ColorStates { + /// The default color. + pub default: Color, + /// The color when the mouse is hovering over the element. + pub hover: Color, + /// The color when the mouse button is held down on the element. + pub active: Color, + /// The color when the element is focused with the keyboard. + pub focused: Color, + /// The color when the element is disabled. + pub disabled: Color, +} + +/// Returns a set of colors for different states of an element. +/// +/// todo!("Test and improve this function") +pub fn states_for_color(color: Color, is_light: bool) -> ColorStates { + let hover_lightness = if is_light { 0.9 } else { 0.1 }; + let active_lightness = if is_light { 0.8 } else { 0.2 }; + let focused_lightness = if is_light { 0.7 } else { 0.3 }; + let disabled_lightness = if is_light { 0.6 } else { 0.5 }; + + let hover = color.mix(hsl(0.0, 0.0, hover_lightness), 0.1); + let active = color.mix(hsl(0.0, 0.0, active_lightness), 0.1); + let focused = color.mix(hsl(0.0, 0.0, focused_lightness), 0.1); + let disabled = color.mix(hsl(0.0, 0.0, disabled_lightness), 0.1); + + ColorStates { + default: color, + hover, + active, + focused, + disabled, + } +} diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 1c30176b25b41130b79ccbc8879167fe34275895..428bcaac10b76501c78a772652f44370d4a74156 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -34,6 +34,7 @@ story = { path = "../story", optional = true } toml.workspace = true uuid.workspace = true util = { path = "../util" } +color = {path = "../color"} itertools = { version = "0.11.0", optional = true } [dev-dependencies] diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index f8d90b7bdc823b0b52348fe94908454002616347..c40d7c8ceb2a1d99b1e7b97f7efb0b18b9bea48f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -147,3 +147,10 @@ pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla { color.a = alpha; color } + +pub fn to_gpui_hsla(color: color::Color) -> gpui::Hsla { + let hsla = color.value(); + let hue: f32 = hsla.hue.into(); + + gpui::hsla(hue / 360.0, hsla.saturation, hsla.lightness, hsla.alpha) +} From dde0056845d31e1bcc60feb28e63622502017e0a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 16 Jan 2024 01:08:17 -0500 Subject: [PATCH 12/96] Use srgb, get mix and blend working --- crates/color/src/color.rs | 160 +++++++++++++++++++++++++------------- crates/theme/src/theme.rs | 7 -- 2 files changed, 107 insertions(+), 60 deletions(-) diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs index 8a7f3f17525cd097dfb4072c43b83675f54323c3..d3d832099af16d85f2afd0e5e6551bca966618c6 100644 --- a/crates/color/src/color.rs +++ b/crates/color/src/color.rs @@ -7,7 +7,9 @@ //! **Note:** This crate does not depend on `gpui`, so it does not provide any //! interfaces for converting to `gpui` style colors. -use palette::{FromColor, Hsl, Hsla, Mix, Srgba, WithAlpha}; +use palette::{ + blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha, +}; #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum BlendMode { @@ -24,16 +26,11 @@ pub enum BlendMode { Exclusion, } -/// Creates a new [`palette::Hsl`] color. -pub fn hsl(h: f32, s: f32, l: f32) -> Hsl { - Hsl::new_srgb(h, s, l) -} - /// Converts a hexadecimal color string to a `palette::Hsla` color. /// /// This function supports the following hex formats: /// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`. -pub fn hex_to_hsla(s: &str) -> Result { +pub fn hex_to_hsla(s: &str) -> Result { let hex = s.trim_start_matches('#'); // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA @@ -64,64 +61,112 @@ pub fn hex_to_hsla(s: &str) -> Result { let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0; let a = (hex_val & 0xFF) as f32 / 255.0; - let srgba = Srgba::new(r, g, b, a); - let hsl = Hsl::from_color(srgba); - let hsla = Hsla::from(hsl).with_alpha(a); + let color = Color { r, g, b, a }; - Ok(hsla) + Ok(color) } -/// Mixes two [`palette::Hsl`] colors at the given `mix_ratio`. -pub fn hsl_mix(hsla_1: Hsl, hsla_2: Hsl, mix_ratio: f32) -> Hsl { - hsla_1.mix(hsla_2, mix_ratio).into() +// This implements conversion to and from all Palette colors. +#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)] +// We have to tell Palette that we will take care of converting to/from sRGB. +#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")] +pub struct Color { + r: f32, + g: f32, + b: f32, + // Let Palette know this is our alpha channel. + #[palette(alpha)] + a: f32, } -/// Represents a color -/// An interstitial state used to provide a consistent API for colors -/// with additional functionality like color mixing, blending, etc. -/// -/// Does not return [gpui] colors as the `color` crate does not -/// depend on [gpui]. -#[derive(Debug, Copy, Clone)] -pub struct Color { - value: Hsla, +// There's no blanket implementation for Self -> Self, unlike the From trait. +// This is to better allow cases like Self -> Self. +impl FromColorUnclamped for Color { + fn from_color_unclamped(color: Color) -> Color { + color + } } -impl Color { - /// Creates a new [`Color`] with an alpha value. - pub fn new(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self { - Self { - value: Hsla::new(hue, saturation, lightness, alpha), +// Convert from any kind of f32 sRGB. +impl FromColorUnclamped> for Color +where + Srgb: FromColorUnclamped>, +{ + fn from_color_unclamped(color: Rgb) -> Color { + let srgb = Srgb::from_color_unclamped(color); + Color { + r: srgb.red, + g: srgb.green, + b: srgb.blue, + a: 1.0, } } +} + +// Convert into any kind of f32 sRGB. +impl FromColorUnclamped for Rgb +where + Rgb: FromColorUnclamped, +{ + fn from_color_unclamped(color: Color) -> Self { + let srgb = Srgb::new(color.r, color.g, color.b); + Self::from_color_unclamped(srgb) + } +} - /// Creates a new [`Color`] with an alpha value of `1.0`. - pub fn hsl(hue: f32, saturation: f32, lightness: f32) -> Self { - Self::new(hue, saturation, lightness, 1.0) +// Add the required clamping. +impl Clamp for Color { + fn clamp(self) -> Self { + Color { + r: self.r.min(1.0).max(0.0), + g: self.g.min(1.0).max(0.0), + b: self.b.min(1.0).max(0.0), + a: self.a.min(1.0).max(0.0), + } } +} - /// Returns the [`palette::Hsla`] value of this color. - pub fn value(&self) -> Hsla { - self.value +impl Color { + pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { + Color { r, g, b, a } } /// Returns a set of states for this color. - pub fn states(&self, is_light: bool) -> ColorStates { - states_for_color(*self, is_light) + pub fn states(self, is_light: bool) -> ColorStates { + states_for_color(self, is_light) } /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`. - pub fn mix(&self, other: Hsl, mix_ratio: f32) -> Self { - let mixed = self.value.mix(other.into(), mix_ratio); + pub fn mixed(&self, other: Color, mix_ratio: f32) -> Self { + let srgb_self = Srgb::new(self.r, self.g, self.b); + let srgb_other = Srgb::new(other.r, other.g, other.b); + + // Directly mix the colors as sRGB values + let mixed = srgb_self.mix(srgb_other, mix_ratio); + Color::from_color_unclamped(mixed) + } + + pub fn blend(&self, other: Color, blend_mode: BlendMode) -> Self { + let srgb_self = Srgb::new(self.r, self.g, self.b); + let srgb_other = Srgb::new(other.r, other.g, other.b); + + let blended = match blend_mode { + // replace hsl methods with the respective sRGB methods + BlendMode::Multiply => srgb_self.multiply(srgb_other), + _ => unimplemented!(), + }; Self { - value: mixed.into(), + r: blended.red, + g: blended.green, + b: blended.blue, + a: self.a, } } } /// A set of colors for different states of an element. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone)] pub struct ColorStates { /// The default color. pub default: Color, @@ -139,21 +184,30 @@ pub struct ColorStates { /// /// todo!("Test and improve this function") pub fn states_for_color(color: Color, is_light: bool) -> ColorStates { - let hover_lightness = if is_light { 0.9 } else { 0.1 }; - let active_lightness = if is_light { 0.8 } else { 0.2 }; - let focused_lightness = if is_light { 0.7 } else { 0.3 }; - let disabled_lightness = if is_light { 0.6 } else { 0.5 }; + let adjustment_factor = if is_light { 0.1 } else { -0.1 }; + let hover_adjustment = 1.0 - adjustment_factor; + let active_adjustment = 1.0 - 2.0 * adjustment_factor; + let focused_adjustment = 1.0 - 3.0 * adjustment_factor; + let disabled_adjustment = 1.0 - 4.0 * adjustment_factor; + + let make_adjustment = |color: Color, adjustment: f32| -> Color { + // Adjust lightness for each state + // Note: Adjustment logic may differ; simplify as needed for sRGB + Color::new( + color.r * adjustment, + color.g * adjustment, + color.b * adjustment, + color.a, + ) + }; - let hover = color.mix(hsl(0.0, 0.0, hover_lightness), 0.1); - let active = color.mix(hsl(0.0, 0.0, active_lightness), 0.1); - let focused = color.mix(hsl(0.0, 0.0, focused_lightness), 0.1); - let disabled = color.mix(hsl(0.0, 0.0, disabled_lightness), 0.1); + let color = color.clamp(); ColorStates { - default: color, - hover, - active, - focused, - disabled, + default: color.clone(), + hover: make_adjustment(color.clone(), hover_adjustment), + active: make_adjustment(color.clone(), active_adjustment), + focused: make_adjustment(color.clone(), focused_adjustment), + disabled: make_adjustment(color.clone(), disabled_adjustment), } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c40d7c8ceb2a1d99b1e7b97f7efb0b18b9bea48f..f8d90b7bdc823b0b52348fe94908454002616347 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -147,10 +147,3 @@ pub fn color_alpha(color: Hsla, alpha: f32) -> Hsla { color.a = alpha; color } - -pub fn to_gpui_hsla(color: color::Color) -> gpui::Hsla { - let hsla = color.value(); - let hue: f32 = hsla.hue.into(); - - gpui::hsla(hue / 360.0, hsla.saturation, hsla.lightness, hsla.alpha) -} From 4f25df6ce263b5ea770f2a51d1534b7dd5dc81bf Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 16 Jan 2024 11:22:14 -0500 Subject: [PATCH 13/96] Prevent div background/content/border from interleaving at same z-index Co-Authored-By: Antonio Scandurra --- crates/gpui/src/style.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 9f46d8dab6fd6214b0f22cf0b4e616300d595e8a..8fdb926b27ebc8e1ab6b4531a5902e0395f53f8b 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -386,7 +386,7 @@ impl Style { let background_color = self.background.as_ref().and_then(Fill::color); if background_color.map_or(false, |color| !color.is_transparent()) { - cx.with_z_index(1, |cx| { + cx.with_z_index(0, |cx| { let mut border_color = background_color.unwrap_or_default(); border_color.a = 0.; cx.paint_quad(quad( @@ -399,12 +399,12 @@ impl Style { }); } - cx.with_z_index(2, |cx| { + cx.with_z_index(0, |cx| { continuation(cx); }); if self.is_border_visible() { - cx.with_z_index(3, |cx| { + cx.with_z_index(0, |cx| { let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size); let border_widths = self.border_widths.to_pixels(rem_size); let max_border_width = border_widths.max(); From 60b79ef2ea90f294ee905b24612d3873da96a889 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 16 Jan 2024 11:23:28 -0500 Subject: [PATCH 14/96] Prevent content mask breaks from having the same z-index Co-Authored-By: Antonio Scandurra --- crates/gpui/src/window.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 869d6b18268cc64bc18f9187dd34e8032237d28b..0269ccfb6c8ef55df1696157302f6156e041f744 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -315,6 +315,7 @@ pub(crate) struct Frame { pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds)>, pub(crate) z_index_stack: StackingOrder, pub(crate) next_stacking_order_id: u32, + next_root_z_index: u8, content_mask_stack: Vec>, element_offset_stack: Vec>, requested_input_handler: Option, @@ -337,6 +338,7 @@ impl Frame { depth_map: Vec::new(), z_index_stack: StackingOrder::default(), next_stacking_order_id: 0, + next_root_z_index: 0, content_mask_stack: Vec::new(), element_offset_stack: Vec::new(), requested_input_handler: None, @@ -354,6 +356,7 @@ impl Frame { self.dispatch_tree.clear(); self.depth_map.clear(); self.next_stacking_order_id = 0; + self.next_root_z_index = 0; self.reused_views.clear(); self.scene.clear(); self.requested_input_handler.take(); @@ -2450,8 +2453,13 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { }; let new_stacking_order_id = post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); + let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index); let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack); self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; + self.window_mut() + .next_frame + .z_index_stack + .push(new_root_z_index); self.window_mut().next_frame.content_mask_stack.push(mask); let result = f(self); self.window_mut().next_frame.content_mask_stack.pop(); From db433586aacd65780826325125aea895b1ed34ee Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 16 Jan 2024 16:52:55 -0800 Subject: [PATCH 15/96] Add some small test code for tracking down this list bug --- crates/gpui/src/app/test_context.rs | 12 +- crates/gpui/src/element.rs | 6 + crates/gpui/src/elements/list.rs | 227 ++++++++++++++++++---------- crates/gpui/src/interactive.rs | 7 +- crates/gpui/src/window.rs | 1 + 5 files changed, 163 insertions(+), 90 deletions(-) diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 17c2a573a89e84573fc4667ae40964000c2ac3b7..3b01096bb5ad76ab250dc8df9e8fcb42dc6ff3e2 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -3,9 +3,9 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor, - IntoElement, Keystroke, Model, ModelContext, Pixels, Platform, Render, Result, Size, Task, - TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, - WindowContext, WindowHandle, WindowOptions, + InputEvent, IntoElement, Keystroke, Model, ModelContext, Pixels, Platform, Render, Result, + Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, + VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -609,6 +609,12 @@ impl<'a> VisualTestContext { self.cx.simulate_input(self.window, input) } + /// Simulate an event from the platform, e.g. a SrollWheelEvent + pub fn simulate_event(&mut self, event: InputEvent) { + self.update(|cx| cx.dispatch_event(event)); + self.background_executor.run_until_parked(); + } + /// Simulates the user blurring the window. pub fn deactivate_window(&mut self) { if Some(self.window) == self.test_platform.active_window() { diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 179c2cb1e25db449556e92cfbf9710278dbd2b73..3022f9f30a5fa48d3a3d9b14b06011bdde2cc610 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -115,6 +115,12 @@ pub trait Render: 'static + Sized { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement; } +impl Render for () { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + () + } +} + /// You can derive [`IntoElement`] on any type that implements this trait. /// It is used to allow views to be expressed in terms of abstract data. pub trait RenderOnce: 'static { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 2c076c8bdcdcb77fcc477f82dfba4f04a29bc2f2..7713bd27e1af3f903a512fd9995263b8dda308f3 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -1,6 +1,6 @@ use crate::{ point, px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, - DispatchPhase, Element, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, + DispatchPhase, Element, IntoElement, IsZero, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext, }; use collections::VecDeque; @@ -28,6 +28,7 @@ struct StateInner { render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, + pending_scroll_delta: Pixels, alignment: ListAlignment, overdraw: Pixels, #[allow(clippy::type_complexity)] @@ -92,6 +93,7 @@ impl ListState { alignment: orientation, overdraw, scroll_handler: None, + pending_scroll_delta: px(0.), }))) } @@ -230,6 +232,8 @@ impl StateInner { delta: Point, cx: &mut WindowContext, ) { + // self.pending_scroll_delta += delta.y; + let scroll_max = (self.items.summary().height - height).max(px(0.)); let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) .max(px(0.)) @@ -346,105 +350,119 @@ impl Element for List { height: AvailableSpace::MinContent, }; - // Render items after the scroll top, including those in the trailing overdraw let mut cursor = old_items.cursor::(); - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); - for (ix, item) in cursor.by_ref().enumerate() { - let visible_height = rendered_height - scroll_top.offset_in_item; - if visible_height >= bounds.size.height + state.overdraw { - break; - } - // Use the previously cached height if available - let mut height = if let ListItem::Rendered { height } = item { - Some(*height) - } else { - None - }; - - // If we're within the visible area or the height wasn't cached, render and measure the item's element - if visible_height < bounds.size.height || height.is_none() { - let mut element = (state.render_item)(scroll_top.item_ix + ix, cx); - let element_size = element.measure(available_item_space, cx); - height = Some(element_size.height); - if visible_height < bounds.size.height { - item_elements.push_back(element); + loop { + // Render items after the scroll top, including those in the trailing overdraw + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + for (ix, item) in cursor.by_ref().enumerate() { + let visible_height = rendered_height - scroll_top.offset_in_item; + if visible_height >= bounds.size.height + state.overdraw { + break; + } + + // Use the previously cached height if available + let mut height = if let ListItem::Rendered { height } = item { + Some(*height) + } else { + None + }; + + // If we're within the visible area or the height wasn't cached, render and measure the item's element + if visible_height < bounds.size.height || height.is_none() { + let mut element = (state.render_item)(scroll_top.item_ix + ix, cx); + let element_size = element.measure(available_item_space, cx); + height = Some(element_size.height); + if visible_height < bounds.size.height { + item_elements.push_back(element); + } } + + let height = height.unwrap(); + rendered_height += height; + measured_items.push_back(ListItem::Rendered { height }); } - let height = height.unwrap(); - rendered_height += height; - measured_items.push_back(ListItem::Rendered { height }); - } + // Prepare to start walking upward from the item at the scroll top. + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + + // If the rendered items do not fill the visible region, then adjust + // the scroll top upward. + if rendered_height - scroll_top.offset_in_item < bounds.size.height { + while rendered_height < bounds.size.height { + cursor.prev(&()); + if cursor.item().is_some() { + let mut element = (state.render_item)(cursor.start().0, cx); + let element_size = element.measure(available_item_space, cx); + + rendered_height += element_size.height; + measured_items.push_front(ListItem::Rendered { + height: element_size.height, + }); + item_elements.push_front(element) + } else { + break; + } + } - // Prepare to start walking upward from the item at the scroll top. - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - bounds.size.height, + }; + + match state.alignment { + ListAlignment::Top => { + scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); + state.logical_scroll_top = Some(scroll_top); + } + ListAlignment::Bottom => { + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - bounds.size.height, + }; + state.logical_scroll_top = None; + } + }; + } - // If the rendered items do not fill the visible region, then adjust - // the scroll top upward. - if rendered_height - scroll_top.offset_in_item < bounds.size.height { - while rendered_height < bounds.size.height { + // Measure items in the leading overdraw + let mut leading_overdraw = scroll_top.offset_in_item; + while leading_overdraw < state.overdraw { cursor.prev(&()); - if cursor.item().is_some() { - let mut element = (state.render_item)(cursor.start().0, cx); - let element_size = element.measure(available_item_space, cx); + if let Some(item) = cursor.item() { + let height = if let ListItem::Rendered { height } = item { + *height + } else { + let mut element = (state.render_item)(cursor.start().0, cx); + element.measure(available_item_space, cx).height + }; - rendered_height += element_size.height; - measured_items.push_front(ListItem::Rendered { - height: element_size.height, - }); - item_elements.push_front(element) + leading_overdraw += height; + measured_items.push_front(ListItem::Rendered { height }); } else { break; } } - scroll_top = ListOffset { - item_ix: cursor.start().0, - offset_in_item: rendered_height - bounds.size.height, - }; + let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); + let mut cursor = old_items.cursor::(); + let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &()); + new_items.extend(measured_items, &()); + cursor.seek(&Count(measured_range.end), Bias::Right, &()); + new_items.append(cursor.suffix(&()), &()); - match state.alignment { - ListAlignment::Top => { - scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); - state.logical_scroll_top = Some(scroll_top); - } - ListAlignment::Bottom => { - scroll_top = ListOffset { - item_ix: cursor.start().0, - offset_in_item: rendered_height - bounds.size.height, - }; - state.logical_scroll_top = None; - } - }; - } + state.items = new_items; + state.last_layout_bounds = Some(bounds); - // Measure items in the leading overdraw - let mut leading_overdraw = scroll_top.offset_in_item; - while leading_overdraw < state.overdraw { - cursor.prev(&()); - if let Some(item) = cursor.item() { - let height = if let ListItem::Rendered { height } = item { - *height - } else { - let mut element = (state.render_item)(cursor.start().0, cx); - element.measure(available_item_space, cx).height - }; + // if !state.pending_scroll_delta.is_zero() { + // // Do scroll manipulation - leading_overdraw += height; - measured_items.push_front(ListItem::Rendered { height }); - } else { - break; - } + // state.pending_scroll_delta = px(0.); + // } else { + break; + // } } - let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); - let mut cursor = old_items.cursor::(); - let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &()); - new_items.extend(measured_items, &()); - cursor.seek(&Count(measured_range.end), Bias::Right, &()); - new_items.append(cursor.suffix(&()), &()); - // Paint the visible items cx.with_content_mask(Some(ContentMask { bounds }), |cx| { let mut item_origin = bounds.origin; @@ -456,12 +474,12 @@ impl Element for List { } }); - state.items = new_items; - state.last_layout_bounds = Some(bounds); - let list_state = self.state.clone(); let height = bounds.size.height; + dbg!("scroll is being bound"); + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + dbg!("scroll dispatched!"); if phase == DispatchPhase::Bubble && bounds.contains(&event.position) && cx.was_top_layer(&event.position, cx.stacking_order()) @@ -562,3 +580,44 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height { self.0.partial_cmp(&other.height).unwrap() } } + +#[cfg(test)] +mod test { + + use crate::{self as gpui, Entity, TestAppContext}; + + #[gpui::test] + fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) { + use crate::{div, list, point, px, size, Element, ListState, Styled}; + + let (v, cx) = cx.add_window_view(|_| ()); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| { + div().h(px(10.)).w_full().into_any() + }); + + cx.update(|cx| { + cx.with_view_id(v.entity_id(), |cx| { + list(state.clone()) + .w_full() + .h_full() + .z_index(10) + .into_any() + .draw(point(px(0.0), px(0.0)), size(px(100.), px(20.)).into(), cx) + }); + }); + + state.reset(5); + + cx.simulate_event(gpui::InputEvent::ScrollWheel(gpui::ScrollWheelEvent { + position: point(px(1.), px(1.)), + delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-500.))), + ..Default::default() + })); + + assert_eq!(state.logical_scroll_top().item_ix, 0); + assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); + + panic!("We should not get here yet!") + } +} diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index dfccfc35307f1eb2e75e2d7e8fe8eb73b2c4b7ef..419be9ac3860e3c33c0c27d58754054000d24a15 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -2,7 +2,7 @@ use crate::{ div, point, Element, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, }; use smallvec::SmallVec; -use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; +use std::{any::Any, default, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct KeyDownEvent { @@ -30,9 +30,10 @@ impl Deref for ModifiersChangedEvent { /// The phase of a touch motion event. /// Based on the winit enum of the same name. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub enum TouchPhase { Started, + #[default] Moved, Ended, } @@ -136,7 +137,7 @@ impl MouseMoveEvent { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct ScrollWheelEvent { pub position: Point, pub delta: ScrollDelta, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 869d6b18268cc64bc18f9187dd34e8032237d28b..df787603471ca1920623dbee8e3d8e0e6d3a3b86 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1716,6 +1716,7 @@ impl<'a> WindowContext<'a> { .mouse_listeners .remove(&event.type_id()) { + dbg!(handlers.len()); // Because handlers may add other handlers, we sort every time. handlers.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); From 8be798d1c0980c8a614226ad2846802bb727b1c6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Jan 2024 19:47:59 -0700 Subject: [PATCH 16/96] Limit number of collaborators in local Facepiles --- crates/collab_ui/src/collab_titlebar_item.rs | 44 ++++++++++++++------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 077e08fac78960449824ddb3473e74f916b7577c..6bbb02c703c4327146b66c4b3aaa3815a1d8611e 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -3,7 +3,7 @@ use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, User, UserStore}; use gpui::{ - actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla, + actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowBounds, @@ -480,7 +480,9 @@ impl CollabTitlebarItem { return None; } + const FACEPILE_LIMIT: usize = 3; let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); + let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT); let pile = FacePile::default() .child( @@ -502,18 +504,34 @@ impl CollabTitlebarItem { ) }), ) - .children(followers.iter().filter_map(|follower_peer_id| { - let follower = room - .remote_participants() - .values() - .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user)) - .or_else(|| { - (self.client.peer_id() == Some(*follower_peer_id)).then_some(current_user) - })? - .clone(); - - Some(Avatar::new(follower.avatar_uri.clone())) - })); + .children( + followers + .iter() + .take(FACEPILE_LIMIT) + .filter_map(|follower_peer_id| { + let follower = room + .remote_participants() + .values() + .find_map(|p| (p.peer_id == *follower_peer_id).then_some(&p.user)) + .or_else(|| { + (self.client.peer_id() == Some(*follower_peer_id)) + .then_some(current_user) + })? + .clone(); + + Some(Avatar::new(follower.avatar_uri.clone())) + }), + ) + .children(if extra_count > 0 { + Some( + div() + .ml_1() + .child(Label::new(format!("+{extra_count}"))) + .into_any_element(), + ) + } else { + None + }); Some(pile) } From 1d5b237b642ed4049eb2066eebd3acd2acf7e6b4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 16 Jan 2024 19:35:26 -0700 Subject: [PATCH 17/96] Allow leaving calls once project is unshared --- crates/collab_ui/src/collab_titlebar_item.rs | 9 +++------ crates/workspace/src/workspace.rs | 8 ++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 077e08fac78960449824ddb3473e74f916b7577c..6c0718f41e62e89116dbe4129e48f4b93d1e6ea9 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -3,7 +3,7 @@ use auto_update::AutoUpdateStatus; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, User, UserStore}; use gpui::{ - actions, canvas, div, point, px, rems, Action, AnyElement, AppContext, Element, Hsla, + actions, canvas, div, point, px, Action, AnyElement, AppContext, Element, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render, StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView, WindowBounds, @@ -19,7 +19,7 @@ use ui::{ }; use util::ResultExt; use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu}; -use workspace::{notifications::NotifyResultExt, Workspace}; +use workspace::{notifications::NotifyResultExt, titlebar_height, Workspace}; const MAX_PROJECT_NAME_LENGTH: usize = 40; const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -62,10 +62,7 @@ impl Render for CollabTitlebarItem { .id("titlebar") .justify_between() .w_full() - .h(rems(1.75)) - // Set a non-scaling min-height here to ensure the titlebar is - // always at least the height of the traffic lights. - .min_h(px(32.)) + .h(titlebar_height(cx)) .map(|this| { if matches!(cx.window_bounds(), WindowBounds::Fullscreen) { this.pl_2() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f2949f58e5d84233d4d2fc36ba8fc1141cacab0c..1d06db5de3b459b289d3b0392c3fb91ac594a760 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -25,7 +25,7 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView, + actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, ManagedView, Model, @@ -4302,6 +4302,10 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { Some(size((width as f64).into(), (height as f64).into())) } +pub fn titlebar_height(cx: &mut WindowContext) -> Pixels { + (1.75 * cx.rem_size()).max(px(32.)) +} + struct DisconnectedOverlay; impl Element for DisconnectedOverlay { @@ -4318,7 +4322,7 @@ impl Element for DisconnectedOverlay { .bg(background) .absolute() .left_0() - .top_0() + .top(titlebar_height(cx)) .size_full() .flex() .items_center() From cae35d3334adea86d560953cd02388a10bb9b313 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 16 Jan 2024 22:19:55 -0800 Subject: [PATCH 18/96] Fix draw helper, add helper traits for selecting groupings of input events --- crates/gpui/src/app/test_context.rs | 56 ++++--- crates/gpui/src/elements/list.rs | 29 ++-- crates/gpui/src/interactive.rs | 170 ++++++++++++++-------- crates/gpui/src/platform.rs | 6 +- crates/gpui/src/platform/mac/events.rs | 8 +- crates/gpui/src/platform/mac/platform.rs | 10 +- crates/gpui/src/platform/mac/window.rs | 56 +++---- crates/gpui/src/platform/test/platform.rs | 2 +- crates/gpui/src/platform/test/window.rs | 14 +- crates/gpui/src/window.rs | 61 ++++---- 10 files changed, 243 insertions(+), 169 deletions(-) diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 3b01096bb5ad76ab250dc8df9e8fcb42dc6ff3e2..556c104f69b9da1a20b32cb0fa325f5aeb975ce9 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -1,11 +1,11 @@ #![deny(missing_docs)] use crate::{ - div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor, - InputEvent, IntoElement, Keystroke, Model, ModelContext, Pixels, Platform, Render, Result, - Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, - VisualContext, WindowContext, WindowHandle, WindowOptions, + Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, + AvailableSpace, BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, + ForegroundExecutor, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform, Point, + Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View, + ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -167,10 +167,14 @@ impl TestAppContext { } /// Adds a new window with no content. - pub fn add_empty_window(&mut self) -> AnyWindowHandle { + pub fn add_empty_window(&mut self) -> &mut VisualTestContext { let mut cx = self.app.borrow_mut(); - cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {})) - .any_handle + let window = cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| ())); + drop(cx); + let cx = Box::new(VisualTestContext::from_window(*window.deref(), self)); + cx.run_until_parked(); + // it might be nice to try and cleanup these at the end of each test. + Box::leak(cx) } /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used @@ -609,9 +613,32 @@ impl<'a> VisualTestContext { self.cx.simulate_input(self.window, input) } + /// Draw an element to the window. Useful for simulating events or actions + pub fn draw( + &mut self, + origin: Point, + space: Size, + f: impl FnOnce(&mut WindowContext) -> AnyElement, + ) { + self.update(|cx| { + let entity_id = cx + .window + .root_view + .as_ref() + .expect("Can't draw to this window without a root view") + .entity_id(); + cx.with_view_id(entity_id, |cx| { + f(cx).draw(origin, space, cx); + }); + + cx.refresh(); + }) + } + /// Simulate an event from the platform, e.g. a SrollWheelEvent - pub fn simulate_event(&mut self, event: InputEvent) { - self.update(|cx| cx.dispatch_event(event)); + /// Make sure you've called [VisualTestContext::draw] first! + pub fn simulate_event(&mut self, event: E) { + self.update(|cx| cx.dispatch_event(event.to_platform_input())); self.background_executor.run_until_parked(); } @@ -769,12 +796,3 @@ impl AnyWindowHandle { self.update(cx, |_, cx| cx.new_view(build_view)).unwrap() } } - -/// An EmptyView for testing. -pub struct EmptyView {} - -impl Render for EmptyView { - fn render(&mut self, _cx: &mut crate::ViewContext) -> impl IntoElement { - div() - } -} diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 7713bd27e1af3f903a512fd9995263b8dda308f3..96067d24000033e5c3b292eea3d70871887be7cc 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -1,6 +1,6 @@ use crate::{ point, px, AnyElement, AvailableSpace, BorrowAppContext, BorrowWindow, Bounds, ContentMask, - DispatchPhase, Element, IntoElement, IsZero, Pixels, Point, ScrollWheelEvent, Size, Style, + DispatchPhase, Element, IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext, }; use collections::VecDeque; @@ -584,36 +584,33 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height { #[cfg(test)] mod test { - use crate::{self as gpui, Entity, TestAppContext}; + use gpui::{ScrollDelta, ScrollWheelEvent}; + + use crate::{self as gpui, TestAppContext}; #[gpui::test] fn test_reset_after_paint_before_scroll(cx: &mut TestAppContext) { use crate::{div, list, point, px, size, Element, ListState, Styled}; - let (v, cx) = cx.add_window_view(|_| ()); + let cx = cx.add_empty_window(); let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _| { div().h(px(10.)).w_full().into_any() }); - cx.update(|cx| { - cx.with_view_id(v.entity_id(), |cx| { - list(state.clone()) - .w_full() - .h_full() - .z_index(10) - .into_any() - .draw(point(px(0.0), px(0.0)), size(px(100.), px(20.)).into(), cx) - }); - }); + cx.draw( + point(px(0.), px(0.)), + size(px(100.), px(20.)).into(), + |_| list(state.clone()).w_full().h_full().z_index(10).into_any(), + ); state.reset(5); - cx.simulate_event(gpui::InputEvent::ScrollWheel(gpui::ScrollWheelEvent { + cx.simulate_event(ScrollWheelEvent { position: point(px(1.), px(1.)), - delta: gpui::ScrollDelta::Pixels(point(px(0.), px(-500.))), + delta: ScrollDelta::Pixels(point(px(0.), px(-500.))), ..Default::default() - })); + }); assert_eq!(state.logical_scroll_top().item_ix, 0); assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 419be9ac3860e3c33c0c27d58754054000d24a15..86e0a6378e29c290aa7e83ab712f2455997d1d4f 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -1,8 +1,14 @@ use crate::{ - div, point, Element, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, + point, seal::Sealed, IntoElement, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, }; use smallvec::SmallVec; -use std::{any::Any, default, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; +use std::{any::Any, fmt::Debug, ops::Deref, path::PathBuf}; + +pub trait InputEvent: Sealed + 'static { + fn to_platform_input(self) -> PlatformInput; +} +pub trait KeyEvent: InputEvent {} +pub trait MouseEvent: InputEvent {} #[derive(Clone, Debug, Eq, PartialEq)] pub struct KeyDownEvent { @@ -10,16 +16,40 @@ pub struct KeyDownEvent { pub is_held: bool, } +impl Sealed for KeyDownEvent {} +impl InputEvent for KeyDownEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::KeyDown(self) + } +} +impl KeyEvent for KeyDownEvent {} + #[derive(Clone, Debug)] pub struct KeyUpEvent { pub keystroke: Keystroke, } +impl Sealed for KeyUpEvent {} +impl InputEvent for KeyUpEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::KeyUp(self) + } +} +impl KeyEvent for KeyUpEvent {} + #[derive(Clone, Debug, Default)] pub struct ModifiersChangedEvent { pub modifiers: Modifiers, } +impl Sealed for ModifiersChangedEvent {} +impl InputEvent for ModifiersChangedEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ModifiersChanged(self) + } +} +impl KeyEvent for ModifiersChangedEvent {} + impl Deref for ModifiersChangedEvent { type Target = Modifiers; @@ -46,6 +76,14 @@ pub struct MouseDownEvent { pub click_count: usize, } +impl Sealed for MouseDownEvent {} +impl InputEvent for MouseDownEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseDown(self) + } +} +impl MouseEvent for MouseDownEvent {} + #[derive(Clone, Debug, Default)] pub struct MouseUpEvent { pub button: MouseButton, @@ -54,38 +92,20 @@ pub struct MouseUpEvent { pub click_count: usize, } +impl Sealed for MouseUpEvent {} +impl InputEvent for MouseUpEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseUp(self) + } +} +impl MouseEvent for MouseUpEvent {} + #[derive(Clone, Debug, Default)] pub struct ClickEvent { pub down: MouseDownEvent, pub up: MouseUpEvent, } -pub struct Drag -where - R: Fn(&mut V, &mut ViewContext) -> E, - V: 'static, - E: IntoElement, -{ - pub state: S, - pub render_drag_handle: R, - view_element_types: PhantomData<(V, E)>, -} - -impl Drag -where - R: Fn(&mut V, &mut ViewContext) -> E, - V: 'static, - E: Element, -{ - pub fn new(state: S, render_drag_handle: R) -> Self { - Drag { - state, - render_drag_handle, - view_element_types: Default::default(), - } - } -} - #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] pub enum MouseButton { Left, @@ -131,6 +151,14 @@ pub struct MouseMoveEvent { pub modifiers: Modifiers, } +impl Sealed for MouseMoveEvent {} +impl InputEvent for MouseMoveEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseMove(self) + } +} +impl MouseEvent for MouseMoveEvent {} + impl MouseMoveEvent { pub fn dragging(&self) -> bool { self.pressed_button == Some(MouseButton::Left) @@ -145,6 +173,14 @@ pub struct ScrollWheelEvent { pub touch_phase: TouchPhase, } +impl Sealed for ScrollWheelEvent {} +impl InputEvent for ScrollWheelEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::ScrollWheel(self) + } +} +impl MouseEvent for ScrollWheelEvent {} + impl Deref for ScrollWheelEvent { type Target = Modifiers; @@ -202,6 +238,14 @@ pub struct MouseExitEvent { pub modifiers: Modifiers, } +impl Sealed for MouseExitEvent {} +impl InputEvent for MouseExitEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::MouseExited(self) + } +} +impl MouseEvent for MouseExitEvent {} + impl Deref for MouseExitEvent { type Target = Modifiers; @@ -221,7 +265,7 @@ impl ExternalPaths { impl Render for ExternalPaths { fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { - div() // Intentionally left empty because the platform will render icons for the dragged files + () // Intentionally left empty because the platform will render icons for the dragged files } } @@ -240,8 +284,16 @@ pub enum FileDropEvent { Exited, } +impl Sealed for FileDropEvent {} +impl InputEvent for FileDropEvent { + fn to_platform_input(self) -> PlatformInput { + PlatformInput::FileDrop(self) + } +} +impl MouseEvent for FileDropEvent {} + #[derive(Clone, Debug)] -pub enum InputEvent { +pub enum PlatformInput { KeyDown(KeyDownEvent), KeyUp(KeyUpEvent), ModifiersChanged(ModifiersChangedEvent), @@ -253,19 +305,19 @@ pub enum InputEvent { FileDrop(FileDropEvent), } -impl InputEvent { +impl PlatformInput { pub fn position(&self) -> Option> { match self { - InputEvent::KeyDown { .. } => None, - InputEvent::KeyUp { .. } => None, - InputEvent::ModifiersChanged { .. } => None, - InputEvent::MouseDown(event) => Some(event.position), - InputEvent::MouseUp(event) => Some(event.position), - InputEvent::MouseMove(event) => Some(event.position), - InputEvent::MouseExited(event) => Some(event.position), - InputEvent::ScrollWheel(event) => Some(event.position), - InputEvent::FileDrop(FileDropEvent::Exited) => None, - InputEvent::FileDrop( + PlatformInput::KeyDown { .. } => None, + PlatformInput::KeyUp { .. } => None, + PlatformInput::ModifiersChanged { .. } => None, + PlatformInput::MouseDown(event) => Some(event.position), + PlatformInput::MouseUp(event) => Some(event.position), + PlatformInput::MouseMove(event) => Some(event.position), + PlatformInput::MouseExited(event) => Some(event.position), + PlatformInput::ScrollWheel(event) => Some(event.position), + PlatformInput::FileDrop(FileDropEvent::Exited) => None, + PlatformInput::FileDrop( FileDropEvent::Entered { position, .. } | FileDropEvent::Pending { position, .. } | FileDropEvent::Submit { position, .. }, @@ -275,29 +327,29 @@ impl InputEvent { pub fn mouse_event(&self) -> Option<&dyn Any> { match self { - InputEvent::KeyDown { .. } => None, - InputEvent::KeyUp { .. } => None, - InputEvent::ModifiersChanged { .. } => None, - InputEvent::MouseDown(event) => Some(event), - InputEvent::MouseUp(event) => Some(event), - InputEvent::MouseMove(event) => Some(event), - InputEvent::MouseExited(event) => Some(event), - InputEvent::ScrollWheel(event) => Some(event), - InputEvent::FileDrop(event) => Some(event), + PlatformInput::KeyDown { .. } => None, + PlatformInput::KeyUp { .. } => None, + PlatformInput::ModifiersChanged { .. } => None, + PlatformInput::MouseDown(event) => Some(event), + PlatformInput::MouseUp(event) => Some(event), + PlatformInput::MouseMove(event) => Some(event), + PlatformInput::MouseExited(event) => Some(event), + PlatformInput::ScrollWheel(event) => Some(event), + PlatformInput::FileDrop(event) => Some(event), } } pub fn keyboard_event(&self) -> Option<&dyn Any> { match self { - InputEvent::KeyDown(event) => Some(event), - InputEvent::KeyUp(event) => Some(event), - InputEvent::ModifiersChanged(event) => Some(event), - InputEvent::MouseDown(_) => None, - InputEvent::MouseUp(_) => None, - InputEvent::MouseMove(_) => None, - InputEvent::MouseExited(_) => None, - InputEvent::ScrollWheel(_) => None, - InputEvent::FileDrop(_) => None, + PlatformInput::KeyDown(event) => Some(event), + PlatformInput::KeyUp(event) => Some(event), + PlatformInput::ModifiersChanged(event) => Some(event), + PlatformInput::MouseDown(_) => None, + PlatformInput::MouseUp(_) => None, + PlatformInput::MouseMove(_) => None, + PlatformInput::MouseExited(_) => None, + PlatformInput::ScrollWheel(_) => None, + PlatformInput::FileDrop(_) => None, } } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 5a2335919ebe6ff9b2277e02d4f6f55b4dc9a80c..8a2e3ea272d392f4db1ba777523ff70b225c1539 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -7,7 +7,7 @@ mod test; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics, - FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap, LineLayout, Pixels, + FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString, Size, TaskLabel, }; @@ -88,7 +88,7 @@ pub(crate) trait Platform: 'static { fn on_resign_active(&self, callback: Box); fn on_quit(&self, callback: Box); fn on_reopen(&self, callback: Box); - fn on_event(&self, callback: Box bool>); + fn on_event(&self, callback: Box bool>); fn set_menus(&self, menus: Vec

, keymap: &Keymap); fn on_app_menu_action(&self, callback: Box); @@ -155,7 +155,7 @@ pub trait PlatformWindow { fn zoom(&self); fn toggle_full_screen(&self); fn on_request_frame(&self, callback: Box); - fn on_input(&self, callback: Box bool>); + fn on_input(&self, callback: Box bool>); fn on_active_status_change(&self, callback: Box); fn on_resize(&self, callback: Box, f32)>); fn on_fullscreen(&self, callback: Box); diff --git a/crates/gpui/src/platform/mac/events.rs b/crates/gpui/src/platform/mac/events.rs index c67018ad5d4666ec8ae15511e4739f811ebcf309..f84833d3cbb1678c1ee1ee9b0c1793bb9df8bae0 100644 --- a/crates/gpui/src/platform/mac/events.rs +++ b/crates/gpui/src/platform/mac/events.rs @@ -1,7 +1,7 @@ use crate::{ - point, px, InputEvent, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, - MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, - Pixels, ScrollDelta, ScrollWheelEvent, TouchPhase, + point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, + PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase, }; use cocoa::{ appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, @@ -82,7 +82,7 @@ unsafe fn read_modifiers(native_event: id) -> Modifiers { } } -impl InputEvent { +impl PlatformInput { pub unsafe fn from_native(native_event: id, window_height: Option) -> Option { let event_type = native_event.eventType(); diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 8061cc136064c8624d4e8ad217d0a1703274001f..499ac0b59104d9ab0b8204a95c01371a228a6b47 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,8 +1,8 @@ use super::{events::key_to_native, BoolExt}; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, - ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, - MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, + ForegroundExecutor, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, + MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformInput, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; @@ -153,7 +153,7 @@ pub struct MacPlatformState { resign_active: Option>, reopen: Option>, quit: Option>, - event: Option bool>>, + event: Option bool>>, menu_command: Option>, validate_menu_command: Option bool>>, will_open_menu: Option>, @@ -637,7 +637,7 @@ impl Platform for MacPlatform { self.0.lock().reopen = Some(callback); } - fn on_event(&self, callback: Box bool>) { + fn on_event(&self, callback: Box bool>) { self.0.lock().event = Some(callback); } @@ -976,7 +976,7 @@ unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform { extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { unsafe { - if let Some(event) = InputEvent::from_native(native_event, None) { + if let Some(event) = PlatformInput::from_native(native_event, None) { let platform = get_mac_platform(this); let mut lock = platform.0.lock(); if let Some(mut callback) = lock.event.take() { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index c364021281a3690635713bd756c520b1d5f3558b..134390bb79900b0cc09efba720b373303d8cd26b 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1,9 +1,9 @@ use super::{display_bounds_from_native, ns_string, MacDisplay, MetalRenderer, NSRange}; use crate::{ display_bounds_to_native, point, px, size, AnyWindowHandle, Bounds, ExternalPaths, - FileDropEvent, ForegroundExecutor, GlobalPixels, InputEvent, KeyDownEvent, Keystroke, - Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, + FileDropEvent, ForegroundExecutor, GlobalPixels, KeyDownEvent, Keystroke, Modifiers, + ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, + PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PromptLevel, Size, Timer, WindowAppearance, WindowBounds, WindowKind, WindowOptions, }; use block::ConcreteBlock; @@ -319,7 +319,7 @@ struct MacWindowState { renderer: MetalRenderer, kind: WindowKind, request_frame_callback: Option>, - event_callback: Option bool>>, + event_callback: Option bool>>, activate_callback: Option>, resize_callback: Option, f32)>>, fullscreen_callback: Option>, @@ -333,7 +333,7 @@ struct MacWindowState { synthetic_drag_counter: usize, last_fresh_keydown: Option, traffic_light_position: Option>, - previous_modifiers_changed_event: Option, + previous_modifiers_changed_event: Option, // State tracking what the IME did after the last request ime_state: ImeState, // Retains the last IME Text @@ -928,7 +928,7 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().request_frame_callback = Some(callback); } - fn on_input(&self, callback: Box bool>) { + fn on_input(&self, callback: Box bool>) { self.0.as_ref().lock().event_callback = Some(callback); } @@ -1053,9 +1053,9 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: let mut lock = window_state.as_ref().lock(); let window_height = lock.content_size().height; - let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) }; + let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) }; - if let Some(InputEvent::KeyDown(event)) = event { + if let Some(PlatformInput::KeyDown(event)) = event { // For certain keystrokes, macOS will first dispatch a "key equivalent" event. // If that event isn't handled, it will then dispatch a "key down" event. GPUI // makes no distinction between these two types of events, so we need to ignore @@ -1102,7 +1102,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent: .flatten() .is_some(); if !is_composing { - handled = callback(InputEvent::KeyDown(event)); + handled = callback(PlatformInput::KeyDown(event)); } if !handled { @@ -1146,11 +1146,11 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let is_active = unsafe { lock.native_window.isKeyWindow() == YES }; let window_height = lock.content_size().height; - let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) }; + let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) }; if let Some(mut event) = event { match &mut event { - InputEvent::MouseDown( + PlatformInput::MouseDown( event @ MouseDownEvent { button: MouseButton::Left, modifiers: Modifiers { control: true, .. }, @@ -1172,7 +1172,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { // Because we map a ctrl-left_down to a right_down -> right_up let's ignore // the ctrl-left_up to avoid having a mismatch in button down/up events if the // user is still holding ctrl when releasing the left mouse button - InputEvent::MouseUp( + PlatformInput::MouseUp( event @ MouseUpEvent { button: MouseButton::Left, modifiers: Modifiers { control: true, .. }, @@ -1194,7 +1194,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { }; match &event { - InputEvent::MouseMove( + PlatformInput::MouseMove( event @ MouseMoveEvent { pressed_button: Some(_), .. @@ -1216,15 +1216,15 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { } } - InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return, + PlatformInput::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return, - InputEvent::MouseUp(MouseUpEvent { .. }) => { + PlatformInput::MouseUp(MouseUpEvent { .. }) => { lock.synthetic_drag_counter += 1; } - InputEvent::ModifiersChanged(ModifiersChangedEvent { modifiers }) => { + PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers }) => { // Only raise modifiers changed event when they have actually changed - if let Some(InputEvent::ModifiersChanged(ModifiersChangedEvent { + if let Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent { modifiers: prev_modifiers, })) = &lock.previous_modifiers_changed_event { @@ -1258,7 +1258,7 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) { key: ".".into(), ime_key: None, }; - let event = InputEvent::KeyDown(KeyDownEvent { + let event = PlatformInput::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), is_held: false, }); @@ -1655,7 +1655,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr if send_new_event(&window_state, { let position = drag_event_position(&window_state, dragging_info); let paths = external_paths_from_event(dragging_info); - InputEvent::FileDrop(FileDropEvent::Entered { position, paths }) + PlatformInput::FileDrop(FileDropEvent::Entered { position, paths }) }) { window_state.lock().external_files_dragged = true; NSDragOperationCopy @@ -1669,7 +1669,7 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr let position = drag_event_position(&window_state, dragging_info); if send_new_event( &window_state, - InputEvent::FileDrop(FileDropEvent::Pending { position }), + PlatformInput::FileDrop(FileDropEvent::Pending { position }), ) { NSDragOperationCopy } else { @@ -1679,7 +1679,10 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; - send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited)); + send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Exited), + ); window_state.lock().external_files_dragged = false; } @@ -1688,7 +1691,7 @@ extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) - let position = drag_event_position(&window_state, dragging_info); if send_new_event( &window_state, - InputEvent::FileDrop(FileDropEvent::Submit { position }), + PlatformInput::FileDrop(FileDropEvent::Submit { position }), ) { YES } else { @@ -1712,7 +1715,10 @@ fn external_paths_from_event(dragging_info: *mut Object) -> ExternalPaths { extern "C" fn conclude_drag_operation(this: &Object, _: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; - send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited)); + send_new_event( + &window_state, + PlatformInput::FileDrop(FileDropEvent::Exited), + ); } async fn synthetic_drag( @@ -1727,7 +1733,7 @@ async fn synthetic_drag( if lock.synthetic_drag_counter == drag_id { if let Some(mut callback) = lock.event_callback.take() { drop(lock); - callback(InputEvent::MouseMove(event.clone())); + callback(PlatformInput::MouseMove(event.clone())); window_state.lock().event_callback = Some(callback); } } else { @@ -1737,7 +1743,7 @@ async fn synthetic_drag( } } -fn send_new_event(window_state_lock: &Mutex, e: InputEvent) -> bool { +fn send_new_event(window_state_lock: &Mutex, e: PlatformInput) -> bool { let window_state = window_state_lock.lock().event_callback.take(); if let Some(mut callback) = window_state { callback(e); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 3a4f5bb36a1360d7d994e82e9347563f985caa79..f5e2170b28acdeae304495172c7b6bbac568bcf4 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -239,7 +239,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn on_event(&self, _callback: Box bool>) { + fn on_event(&self, _callback: Box bool>) { unimplemented!() } diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index f05e13e3a027e2be9d4f17690fe7162a73396d03..5c8a3e5a59cf91b86d50c311c1beeccfd5ac2840 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -1,7 +1,7 @@ use crate::{ - px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, InputEvent, KeyDownEvent, - Keystroke, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, - Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions, + px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke, + Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, + Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions, }; use collections::HashMap; use parking_lot::Mutex; @@ -19,7 +19,7 @@ pub struct TestWindowState { platform: Weak, sprite_atlas: Arc, pub(crate) should_close_handler: Option bool>>, - input_callback: Option bool>>, + input_callback: Option bool>>, active_status_change_callback: Option>, resize_callback: Option, f32)>>, moved_callback: Option>, @@ -85,7 +85,7 @@ impl TestWindow { self.0.lock().active_status_change_callback = Some(callback); } - pub fn simulate_input(&mut self, event: InputEvent) -> bool { + pub fn simulate_input(&mut self, event: PlatformInput) -> bool { let mut lock = self.0.lock(); let Some(mut callback) = lock.input_callback.take() else { return false; @@ -97,7 +97,7 @@ impl TestWindow { } pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) { - if self.simulate_input(InputEvent::KeyDown(KeyDownEvent { + if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent { keystroke: keystroke.clone(), is_held, })) { @@ -220,7 +220,7 @@ impl PlatformWindow for TestWindow { fn on_request_frame(&self, _callback: Box) {} - fn on_input(&self, callback: Box bool>) { + fn on_input(&self, callback: Box bool>) { self.0.lock().input_callback = Some(callback) } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index df787603471ca1920623dbee8e3d8e0e6d3a3b86..00b17ba3c07ba6db532035c37d5bf7cdd74e9f59 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -5,13 +5,14 @@ use crate::{ AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla, - ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeystrokeEvent, LayoutId, - Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseMoveEvent, MouseUpEvent, - Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, - PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, - RenderSvgParams, ScaledPixels, Scene, Shadow, SharedString, Size, Style, SubscriberSet, - Subscription, Surface, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, - WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + ImageData, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, KeystrokeEvent, LayoutId, + Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseEvent, MouseMoveEvent, + MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, + PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, + RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, Shadow, + SharedString, Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, Task, + Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, + SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::{FxHashMap, FxHashSet}; @@ -968,7 +969,7 @@ impl<'a> WindowContext<'a> { /// Register a mouse event listener on the window for the next frame. The type of event /// is determined by the first parameter of the given listener. When the next frame is rendered /// the listener will be cleared. - pub fn on_mouse_event( + pub fn on_mouse_event( &mut self, mut handler: impl FnMut(&Event, DispatchPhase, &mut WindowContext) + 'static, ) { @@ -996,7 +997,7 @@ impl<'a> WindowContext<'a> { /// /// This is a fairly low-level method, so prefer using event handlers on elements unless you have /// a specific need to register a global listener. - pub fn on_key_event( + pub fn on_key_event( &mut self, listener: impl Fn(&Event, DispatchPhase, &mut WindowContext) + 'static, ) { @@ -1617,7 +1618,7 @@ impl<'a> WindowContext<'a> { } /// Dispatch a mouse or keyboard event on the window. - pub fn dispatch_event(&mut self, event: InputEvent) -> bool { + pub fn dispatch_event(&mut self, event: PlatformInput) -> bool { // Handlers may set this to false by calling `stop_propagation`. self.app.propagate_event = true; // Handlers may set this to true by calling `prevent_default`. @@ -1626,37 +1627,37 @@ impl<'a> WindowContext<'a> { let event = match event { // Track the mouse position with our own state, since accessing the platform // API for the mouse position can only occur on the main thread. - InputEvent::MouseMove(mouse_move) => { + PlatformInput::MouseMove(mouse_move) => { self.window.mouse_position = mouse_move.position; self.window.modifiers = mouse_move.modifiers; - InputEvent::MouseMove(mouse_move) + PlatformInput::MouseMove(mouse_move) } - InputEvent::MouseDown(mouse_down) => { + PlatformInput::MouseDown(mouse_down) => { self.window.mouse_position = mouse_down.position; self.window.modifiers = mouse_down.modifiers; - InputEvent::MouseDown(mouse_down) + PlatformInput::MouseDown(mouse_down) } - InputEvent::MouseUp(mouse_up) => { + PlatformInput::MouseUp(mouse_up) => { self.window.mouse_position = mouse_up.position; self.window.modifiers = mouse_up.modifiers; - InputEvent::MouseUp(mouse_up) + PlatformInput::MouseUp(mouse_up) } - InputEvent::MouseExited(mouse_exited) => { + PlatformInput::MouseExited(mouse_exited) => { self.window.modifiers = mouse_exited.modifiers; - InputEvent::MouseExited(mouse_exited) + PlatformInput::MouseExited(mouse_exited) } - InputEvent::ModifiersChanged(modifiers_changed) => { + PlatformInput::ModifiersChanged(modifiers_changed) => { self.window.modifiers = modifiers_changed.modifiers; - InputEvent::ModifiersChanged(modifiers_changed) + PlatformInput::ModifiersChanged(modifiers_changed) } - InputEvent::ScrollWheel(scroll_wheel) => { + PlatformInput::ScrollWheel(scroll_wheel) => { self.window.mouse_position = scroll_wheel.position; self.window.modifiers = scroll_wheel.modifiers; - InputEvent::ScrollWheel(scroll_wheel) + PlatformInput::ScrollWheel(scroll_wheel) } // Translate dragging and dropping of external files from the operating system // to internal drag and drop events. - InputEvent::FileDrop(file_drop) => match file_drop { + PlatformInput::FileDrop(file_drop) => match file_drop { FileDropEvent::Entered { position, paths } => { self.window.mouse_position = position; if self.active_drag.is_none() { @@ -1666,7 +1667,7 @@ impl<'a> WindowContext<'a> { cursor_offset: position, }); } - InputEvent::MouseMove(MouseMoveEvent { + PlatformInput::MouseMove(MouseMoveEvent { position, pressed_button: Some(MouseButton::Left), modifiers: Modifiers::default(), @@ -1674,7 +1675,7 @@ impl<'a> WindowContext<'a> { } FileDropEvent::Pending { position } => { self.window.mouse_position = position; - InputEvent::MouseMove(MouseMoveEvent { + PlatformInput::MouseMove(MouseMoveEvent { position, pressed_button: Some(MouseButton::Left), modifiers: Modifiers::default(), @@ -1683,21 +1684,21 @@ impl<'a> WindowContext<'a> { FileDropEvent::Submit { position } => { self.activate(true); self.window.mouse_position = position; - InputEvent::MouseUp(MouseUpEvent { + PlatformInput::MouseUp(MouseUpEvent { button: MouseButton::Left, position, modifiers: Modifiers::default(), click_count: 1, }) } - FileDropEvent::Exited => InputEvent::MouseUp(MouseUpEvent { + FileDropEvent::Exited => PlatformInput::MouseUp(MouseUpEvent { button: MouseButton::Left, position: Point::default(), modifiers: Modifiers::default(), click_count: 1, }), }, - InputEvent::KeyDown(_) | InputEvent::KeyUp(_) => event, + PlatformInput::KeyDown(_) | PlatformInput::KeyUp(_) => event, }; if let Some(any_mouse_event) = event.mouse_event() { @@ -2976,7 +2977,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { /// Add a listener for any mouse event that occurs in the window. /// This is a fairly low level method. /// Typically, you'll want to use methods on UI elements, which perform bounds checking etc. - pub fn on_mouse_event( + pub fn on_mouse_event( &mut self, handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, ) { @@ -2989,7 +2990,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } /// Register a callback to be invoked when the given Key Event is dispatched to the window. - pub fn on_key_event( + pub fn on_key_event( &mut self, handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, ) { From a99ee5e599dbb491982fc052a3f093e536444db0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 16 Jan 2024 22:30:44 -0800 Subject: [PATCH 19/96] Fix test failures --- crates/collab/src/tests/editor_tests.rs | 78 ++++++++++--------------- crates/gpui/src/app/test_context.rs | 5 ++ crates/search/src/buffer_search.rs | 8 +-- 3 files changed, 40 insertions(+), 51 deletions(-) diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 0c3601b07531bf5c77459fd5530a31ba8ef68717..a5fa187d24acc93af3b2ff64dbf2ef96fffa7ea3 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -185,31 +185,27 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - let window_a = cx_a.add_empty_window(); - let editor_a = - window_a.build_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); + let cx_a = cx_a.add_empty_window(); + let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); let mut editor_cx_a = EditorTestContext { - cx: VisualTestContext::from_window(window_a, cx_a), - window: window_a.into(), + cx: cx_a.clone(), + window: cx_a.handle(), editor: editor_a, assertion_cx: AssertionContextManager::new(), }; - let window_b = cx_b.add_empty_window(); - let mut cx_b = VisualTestContext::from_window(window_b, cx_b); - + let cx_b = cx_b.add_empty_window(); // Open a buffer as client B let buffer_b = project_b - .update(&mut cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - let editor_b = window_b.build_view(&mut cx_b, |cx| { - Editor::for_buffer(buffer_b, Some(project_b), cx) - }); + let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); + let mut editor_cx_b = EditorTestContext { - cx: cx_b, - window: window_b.into(), + cx: cx_b.clone(), + window: cx_b.handle(), editor: editor_b, assertion_cx: AssertionContextManager::new(), }; @@ -311,10 +307,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let window_b = cx_b.add_empty_window(); - let editor_b = window_b.build_view(cx_b, |cx| { - Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) - }); + let cx_b = cx_b.add_empty_window(); + let editor_b = + cx_b.new_view(|cx| Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)); let fake_language_server = fake_language_servers.next().await.unwrap(); cx_a.background_executor.run_until_parked(); @@ -323,10 +318,8 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu assert!(!buffer.completion_triggers().is_empty()) }); - let mut cx_b = VisualTestContext::from_window(window_b, cx_b); - // Type a completion trigger character as the guest. - editor_b.update(&mut cx_b, |editor, cx| { + editor_b.update(cx_b, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); editor.handle_input(".", cx); }); @@ -392,8 +385,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu }); // Confirm a completion on the guest. - - editor_b.update(&mut cx_b, |editor, cx| { + editor_b.update(cx_b, |editor, cx| { assert!(editor.context_menu_visible()); editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx); assert_eq!(editor.text(cx), "fn main() { a.first_method() }"); @@ -431,7 +423,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu ); }); - buffer_b.read_with(&mut cx_b, |buffer, _| { + buffer_b.read_with(cx_b, |buffer, _| { assert_eq!( buffer.text(), "use d::SomeTrait;\nfn main() { a.first_method() }" @@ -960,7 +952,7 @@ async fn test_share_project( cx_c: &mut TestAppContext, ) { let executor = cx_a.executor(); - let window_b = cx_b.add_empty_window(); + let cx_b = cx_b.add_empty_window(); let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; @@ -1075,7 +1067,7 @@ async fn test_share_project( .await .unwrap(); - let editor_b = window_b.build_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); + let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, None, cx)); // Client A sees client B's selection executor.run_until_parked(); @@ -1089,8 +1081,7 @@ async fn test_share_project( }); // Edit the buffer as client B and see that edit as client A. - let mut cx_b = VisualTestContext::from_window(window_b, cx_b); - editor_b.update(&mut cx_b, |editor, cx| editor.handle_input("ok, ", cx)); + editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx)); executor.run_until_parked(); buffer_a.read_with(cx_a, |buffer, _| { @@ -1099,7 +1090,7 @@ async fn test_share_project( // Client B can invite client C on a project shared by client A. active_call_b - .update(&mut cx_b, |call, cx| { + .update(cx_b, |call, cx| { call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx) }) .await @@ -1190,12 +1181,8 @@ async fn test_on_input_format_from_host_to_guest( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let window_a = cx_a.add_empty_window(); - let editor_a = window_a - .update(cx_a, |_, cx| { - cx.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)) - }) - .unwrap(); + let cx_a = cx_a.add_empty_window(); + let editor_a = cx_a.new_view(|cx| Editor::for_buffer(buffer_a, Some(project_a.clone()), cx)); let fake_language_server = fake_language_servers.next().await.unwrap(); executor.run_until_parked(); @@ -1226,10 +1213,9 @@ async fn test_on_input_format_from_host_to_guest( .await .unwrap(); - let mut cx_a = VisualTestContext::from_window(window_a, cx_a); // Type a on type formatting trigger character as the guest. cx_a.focus_view(&editor_a); - editor_a.update(&mut cx_a, |editor, cx| { + editor_a.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); editor.handle_input(">", cx); }); @@ -1241,7 +1227,7 @@ async fn test_on_input_format_from_host_to_guest( }); // Undo should remove LSP edits first - editor_a.update(&mut cx_a, |editor, cx| { + editor_a.update(cx_a, |editor, cx| { assert_eq!(editor.text(cx), "fn main() { a>~< }"); editor.undo(&Undo, cx); assert_eq!(editor.text(cx), "fn main() { a> }"); @@ -1252,7 +1238,7 @@ async fn test_on_input_format_from_host_to_guest( assert_eq!(buffer.text(), "fn main() { a> }") }); - editor_a.update(&mut cx_a, |editor, cx| { + editor_a.update(cx_a, |editor, cx| { assert_eq!(editor.text(cx), "fn main() { a> }"); editor.undo(&Undo, cx); assert_eq!(editor.text(cx), "fn main() { a }"); @@ -1323,17 +1309,15 @@ async fn test_on_input_format_from_guest_to_host( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let window_b = cx_b.add_empty_window(); - let editor_b = window_b.build_view(cx_b, |cx| { - Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) - }); + let cx_b = cx_b.add_empty_window(); + let editor_b = cx_b.new_view(|cx| Editor::for_buffer(buffer_b, Some(project_b.clone()), cx)); let fake_language_server = fake_language_servers.next().await.unwrap(); executor.run_until_parked(); - let mut cx_b = VisualTestContext::from_window(window_b, cx_b); + // Type a on type formatting trigger character as the guest. cx_b.focus_view(&editor_b); - editor_b.update(&mut cx_b, |editor, cx| { + editor_b.update(cx_b, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); editor.handle_input(":", cx); }); @@ -1374,7 +1358,7 @@ async fn test_on_input_format_from_guest_to_host( }); // Undo should remove LSP edits first - editor_b.update(&mut cx_b, |editor, cx| { + editor_b.update(cx_b, |editor, cx| { assert_eq!(editor.text(cx), "fn main() { a:~: }"); editor.undo(&Undo, cx); assert_eq!(editor.text(cx), "fn main() { a: }"); @@ -1385,7 +1369,7 @@ async fn test_on_input_format_from_guest_to_host( assert_eq!(buffer.text(), "fn main() { a: }") }); - editor_b.update(&mut cx_b, |editor, cx| { + editor_b.update(cx_b, |editor, cx| { assert_eq!(editor.text(cx), "fn main() { a: }"); editor.undo(&Undo, cx); assert_eq!(editor.text(cx), "fn main() { a }"); diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 556c104f69b9da1a20b32cb0fa325f5aeb975ce9..41cb722081b60b9b404244ed0e12e126dd63b279 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -568,6 +568,11 @@ pub struct VisualTestContext { } impl<'a> VisualTestContext { + /// Get the underlying window handle underlying this context. + pub fn handle(&self) -> AnyWindowHandle { + self.window + } + /// Provides the `WindowContext` for the duration of the closure. pub fn update(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e217a7ab73cd2fd1aaa540f1b56cf13b7ec1c84b..ed2654de36508de7b20f2f522bc51b245354bd73 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1081,7 +1081,7 @@ mod tests { use super::*; use editor::{DisplayPoint, Editor}; - use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext}; + use gpui::{Context, Hsla, TestAppContext, VisualTestContext}; use language::Buffer; use smol::stream::StreamExt as _; use unindent::Unindent as _; @@ -1114,7 +1114,7 @@ mod tests { .unindent(), ) }); - let (_, cx) = cx.add_window_view(|_| EmptyView {}); + let cx = cx.add_empty_window(); let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); let search_bar = cx.new_view(|cx| { @@ -1461,7 +1461,7 @@ mod tests { "Should pick a query with multiple results" ); let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); - let window = cx.add_window(|_| EmptyView {}); + let window = cx.add_window(|_| ()); let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); @@ -1657,7 +1657,7 @@ mod tests { "# .unindent(); let buffer = cx.new_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); - let (_, cx) = cx.add_window_view(|_| EmptyView {}); + let cx = cx.add_empty_window(); let editor = cx.new_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); From 80852c3e182ccdd4e1237b5e7770efa33ea9ec28 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 16 Jan 2024 22:34:15 -0800 Subject: [PATCH 20/96] Add documentation to the new test --- crates/gpui/src/elements/list.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 96067d24000033e5c3b292eea3d70871887be7cc..9d49149e7abe7333f42abe5eb07b32e697098774 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -598,20 +598,30 @@ mod test { div().h(px(10.)).w_full().into_any() }); + // Ensure that the list is scrolled to the top + state.scroll_to(gpui::ListOffset { + item_ix: 0, + offset_in_item: px(0.0), + }); + + // Paint cx.draw( point(px(0.), px(0.)), size(px(100.), px(20.)).into(), |_| list(state.clone()).w_full().h_full().z_index(10).into_any(), ); + // Reset state.reset(5); + // And then recieve a scroll event _before_ the next paint cx.simulate_event(ScrollWheelEvent { position: point(px(1.), px(1.)), delta: ScrollDelta::Pixels(point(px(0.), px(-500.))), ..Default::default() }); + // Scroll position should stay at the top of the list assert_eq!(state.logical_scroll_top().item_ix, 0); assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); From 97bd3e1fde26deed91f6874058a365b97f54a742 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 17 Jan 2024 09:45:46 +0100 Subject: [PATCH 21/96] Fix segfault caused by wrong size of path sprites bytes length Previously, we were using `size_of` but passing the wrong type in (MonochromeSprite instead of PathSprite). This caused us to read outside of the `sprites` smallvec and triggered the segfault. --- .../gpui/src/platform/mac/metal_renderer.rs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 36ded7ba8ecac63007e1e682caef624ee9766e3f..d3a32cc41d2c4b9d00d95a3b2292c7298a08f613 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -14,6 +14,7 @@ use foreign_types::ForeignType; use media::core_video::CVMetalTextureCache; use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; use objc::{self, msg_send, sel, sel_impl}; +use smallvec::SmallVec; use std::{ffi::c_void, mem, ptr, sync::Arc}; const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); @@ -81,7 +82,7 @@ impl MetalRenderer { ]; let unit_vertices = device.new_buffer_with_data( unit_vertices.as_ptr() as *const c_void, - (unit_vertices.len() * mem::size_of::()) as u64, + mem::size_of_val(&unit_vertices) as u64, MTLResourceOptions::StorageModeManaged, ); let instances = device.new_buffer( @@ -339,7 +340,8 @@ impl MetalRenderer { for (texture_id, vertices) in vertices_by_texture_id { align_offset(offset); - let next_offset = *offset + vertices.len() * mem::size_of::>(); + let vertices_bytes_len = mem::size_of_val(vertices.as_slice()); + let next_offset = *offset + vertices_bytes_len; if next_offset > INSTANCE_BUFFER_SIZE { return None; } @@ -372,7 +374,6 @@ impl MetalRenderer { &texture_size as *const Size as *const _, ); - let vertices_bytes_len = mem::size_of::>() * vertices.len(); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; unsafe { ptr::copy_nonoverlapping( @@ -429,7 +430,7 @@ impl MetalRenderer { &viewport_size as *const Size as *const _, ); - let shadow_bytes_len = std::mem::size_of_val(shadows); + let shadow_bytes_len = mem::size_of_val(shadows); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let next_offset = *offset + shadow_bytes_len; @@ -490,7 +491,7 @@ impl MetalRenderer { &viewport_size as *const Size as *const _, ); - let quad_bytes_len = std::mem::size_of_val(quads); + let quad_bytes_len = mem::size_of_val(quads); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let next_offset = *offset + quad_bytes_len; @@ -537,7 +538,7 @@ impl MetalRenderer { ); let mut prev_texture_id = None; - let mut sprites = Vec::new(); + let mut sprites = SmallVec::<[_; 1]>::new(); let mut paths_and_tiles = paths .iter() .map(|path| (path, tiles_by_path_id.get(&path.id).unwrap())) @@ -590,7 +591,7 @@ impl MetalRenderer { command_encoder .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - let sprite_bytes_len = mem::size_of::() * sprites.len(); + let sprite_bytes_len = mem::size_of_val(sprites.as_slice()); let next_offset = *offset + sprite_bytes_len; if next_offset > INSTANCE_BUFFER_SIZE { return false; @@ -655,17 +656,17 @@ impl MetalRenderer { &viewport_size as *const Size as *const _, ); - let quad_bytes_len = std::mem::size_of_val(underlines); + let underline_bytes_len = mem::size_of_val(underlines); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; unsafe { ptr::copy_nonoverlapping( underlines.as_ptr() as *const u8, buffer_contents, - quad_bytes_len, + underline_bytes_len, ); } - let next_offset = *offset + quad_bytes_len; + let next_offset = *offset + underline_bytes_len; if next_offset > INSTANCE_BUFFER_SIZE { return false; } @@ -726,7 +727,7 @@ impl MetalRenderer { ); command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - let sprite_bytes_len = std::mem::size_of_val(sprites); + let sprite_bytes_len = mem::size_of_val(sprites); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let next_offset = *offset + sprite_bytes_len; @@ -798,7 +799,7 @@ impl MetalRenderer { ); command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - let sprite_bytes_len = std::mem::size_of_val(sprites); + let sprite_bytes_len = mem::size_of_val(sprites); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; let next_offset = *offset + sprite_bytes_len; From 9c337908099ec3c2033722af14245e4ff455e42c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 17 Jan 2024 09:50:55 +0100 Subject: [PATCH 22/96] Check if we exhausted the instance buffer prior to copying underlines This fixes another potential segfault. --- crates/gpui/src/platform/mac/metal_renderer.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index d3a32cc41d2c4b9d00d95a3b2292c7298a08f613..1589757d935894ff676de7371fca97204d1ee63a 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -658,6 +658,12 @@ impl MetalRenderer { let underline_bytes_len = mem::size_of_val(underlines); let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) }; + + let next_offset = *offset + underline_bytes_len; + if next_offset > INSTANCE_BUFFER_SIZE { + return false; + } + unsafe { ptr::copy_nonoverlapping( underlines.as_ptr() as *const u8, @@ -666,11 +672,6 @@ impl MetalRenderer { ); } - let next_offset = *offset + underline_bytes_len; - if next_offset > INSTANCE_BUFFER_SIZE { - return false; - } - command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, 0, From 39dff0e82790c6d99b6c6a9362058d7b5749f63f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 Jan 2024 11:06:46 +0200 Subject: [PATCH 23/96] Stop using button for collab notifications --- crates/workspace/src/pane_group.rs | 74 ++++++++++++++---------------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index ce58e51678c589c39e97666d71ccc097bc0a5324..b6e3b1a433d358db2856e060f140be0654c06baf 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -3,14 +3,14 @@ use anyhow::{anyhow, Result}; use call::{ActiveCall, ParticipantLocation}; use collections::HashMap; use gpui::{ - point, size, AnyView, AnyWeakView, Axis, Bounds, Entity as _, IntoElement, Model, Pixels, + point, size, AnyView, AnyWeakView, Axis, Bounds, IntoElement, Model, MouseButton, Pixels, Point, View, ViewContext, }; use parking_lot::Mutex; use project::Project; use serde::Deserialize; use std::sync::Arc; -use ui::{prelude::*, Button}; +use ui::prelude::*; pub const HANDLE_HITBOX_SIZE: f32 = 4.0; const HORIZONTAL_MIN_SIZE: f32 = 80.; @@ -183,6 +183,7 @@ impl Member { let mut leader_border = None; let mut leader_status_box = None; + let mut leader_join_data = None; if let Some(leader) = &leader { let mut leader_color = cx .theme() @@ -199,44 +200,21 @@ impl Member { if Some(leader_project_id) == project.read(cx).remote_id() { None } else { - let leader_user = leader.user.clone(); - let leader_user_id = leader.user.id; - Some( - Button::new( - ("leader-status", pane.entity_id()), - format!( - "Follow {} to their active project", - leader_user.github_login, - ), - ) - .on_click(cx.listener( - move |this, _, cx| { - crate::join_remote_project( - leader_project_id, - leader_user_id, - this.app_state().clone(), - cx, - ) - .detach_and_log_err(cx); - }, - )), - ) + leader_join_data = Some((leader_project_id, leader.user.id)); + Some(Label::new(format!( + "Follow {} to their active project", + leader.user.github_login, + ))) } } - ParticipantLocation::UnsharedProject => Some(Button::new( - ("leader-status", pane.entity_id()), - format!( - "{} is viewing an unshared Zed project", - leader.user.github_login - ), - )), - ParticipantLocation::External => Some(Button::new( - ("leader-status", pane.entity_id()), - format!( - "{} is viewing a window outside of Zed", - leader.user.github_login - ), - )), + ParticipantLocation::UnsharedProject => Some(Label::new(format!( + "{} is viewing an unshared Zed project", + leader.user.github_login + ))), + ParticipantLocation::External => Some(Label::new(format!( + "{} is viewing a window outside of Zed", + leader.user.github_login + ))), }; } @@ -264,7 +242,25 @@ impl Member { .bottom_3() .right_3() .z_index(1) - .child(status_box), + .bg(cx.theme().colors().element_background) + .child(status_box) + .when_some( + leader_join_data, + |this, (leader_project_id, leader_user_id)| { + this.cursor_pointer().on_mouse_down( + MouseButton::Left, + cx.listener(move |this, _, cx| { + crate::join_remote_project( + leader_project_id, + leader_user_id, + this.app_state().clone(), + cx, + ) + .detach_and_log_err(cx); + }), + ) + }, + ), ) }) .into_any() From 04922d649ccfaf149fa77752ae3d862ccc92d38d Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jan 2024 11:07:20 +0100 Subject: [PATCH 24/96] Fix missing Ctrl-[ bindings in Vim mode This "adds" the keybindings I was missing in Vim mode (e.g. `Ctrl-[` to cancel a selection) by fixing the definitions in the keymap from `Ctrl+[` to `Ctrl-[`. --- assets/keymaps/vim.json | 8 ++++---- crates/vim/src/test.rs | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 81235bb72ad7075be27605c462fb03b822b69140..1da6f0ef8c5da25a60742fda933125c23eac81bf 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -99,7 +99,7 @@ "ctrl-i": "pane::GoForward", "ctrl-]": "editor::GoToDefinition", "escape": ["vim::SwitchMode", "Normal"], - "ctrl+[": ["vim::SwitchMode", "Normal"], + "ctrl-[": ["vim::SwitchMode", "Normal"], "v": "vim::ToggleVisual", "shift-v": "vim::ToggleVisualLine", "ctrl-v": "vim::ToggleVisualBlock", @@ -288,7 +288,7 @@ "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", "bindings": { "escape": "editor::Cancel", - "ctrl+[": "editor::Cancel" + "ctrl-[": "editor::Cancel" } }, { @@ -441,7 +441,7 @@ "r": ["vim::PushOperator", "Replace"], "ctrl-c": ["vim::SwitchMode", "Normal"], "escape": ["vim::SwitchMode", "Normal"], - "ctrl+[": ["vim::SwitchMode", "Normal"], + "ctrl-[": ["vim::SwitchMode", "Normal"], ">": "editor::Indent", "<": "editor::Outdent", "i": [ @@ -481,7 +481,7 @@ "tab": "vim::Tab", "enter": "vim::Enter", "escape": ["vim::SwitchMode", "Normal"], - "ctrl+[": ["vim::SwitchMode", "Normal"] + "ctrl-[": ["vim::SwitchMode", "Normal"] } }, { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index b23c49c9a3fd4f23d1d547f7165df64cb33c1f75..fa2dcb45cda61f4637b5bcb6ac0cd1d43236def3 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -71,6 +71,30 @@ async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { assert_eq!(cx.mode(), Mode::Normal); } +#[gpui::test] +async fn test_cancel_selection(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! {"The quick brown fox juˇmps over the lazy dog"}, + Mode::Normal, + ); + // jumps + cx.simulate_keystrokes(["v", "l", "l"]); + cx.assert_editor_state("The quick brown fox ju«mpsˇ» over the lazy dog"); + + cx.simulate_keystrokes(["escape"]); + cx.assert_editor_state("The quick brown fox jumpˇs over the lazy dog"); + + // go back to the same selection state + cx.simulate_keystrokes(["v", "h", "h"]); + cx.assert_editor_state("The quick brown fox ju«ˇmps» over the lazy dog"); + + // Ctrl-[ should behave like Esc + cx.simulate_keystrokes(["ctrl-["]); + cx.assert_editor_state("The quick brown fox juˇmps over the lazy dog"); +} + #[gpui::test] async fn test_buffer_search(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; From 5b0b9ff5828251e8e85b698bfb0a4437d64e2df6 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jan 2024 13:50:55 +0100 Subject: [PATCH 25/96] Submit bigger primitive batches when rendering Before this change we wouldn't submit all possible primitives of the same kind that are less-than the max order. Result was that we would submit, say, 10 paths each in a separate batch instead of actually batching them. This was overly strict because even if the order of two different primitives was the same, we could have still batched the 1st primitive kind, if its implicit ordering was less than 2nd kind. Example: say we have the following primitives and these orders 5x paths, order 3 2x sprites, order 3 Previously, we would submit 1 path, 1 path, 1 path, 1 path, 1 path, then the sprites. With this changes, we batch the 5 paths into one batch. Co-authored-by: Antonio --- crates/gpui/src/scene.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 11341a2cbc524899a3455f490b20652e7f17fc4b..de031704cd2888917534083bffd90e00c7e8b49b 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -299,8 +299,8 @@ impl<'a> Iterator for BatchIterator<'a> { let first = orders_and_kinds[0]; let second = orders_and_kinds[1]; - let (batch_kind, max_order) = if first.0.is_some() { - (first.1, second.0.unwrap_or(u32::MAX)) + let (batch_kind, max_order_and_kind) = if first.0.is_some() { + (first.1, (second.0.unwrap_or(u32::MAX), second.1)) } else { return None; }; @@ -312,7 +312,7 @@ impl<'a> Iterator for BatchIterator<'a> { self.shadows_iter.next(); while self .shadows_iter - .next_if(|shadow| shadow.order < max_order) + .next_if(|shadow| (shadow.order, batch_kind) < max_order_and_kind) .is_some() { shadows_end += 1; @@ -328,7 +328,7 @@ impl<'a> Iterator for BatchIterator<'a> { self.quads_iter.next(); while self .quads_iter - .next_if(|quad| quad.order < max_order) + .next_if(|quad| (quad.order, batch_kind) < max_order_and_kind) .is_some() { quads_end += 1; @@ -342,7 +342,7 @@ impl<'a> Iterator for BatchIterator<'a> { self.paths_iter.next(); while self .paths_iter - .next_if(|path| path.order < max_order) + .next_if(|path| (path.order, batch_kind) < max_order_and_kind) .is_some() { paths_end += 1; @@ -356,7 +356,7 @@ impl<'a> Iterator for BatchIterator<'a> { self.underlines_iter.next(); while self .underlines_iter - .next_if(|underline| underline.order < max_order) + .next_if(|underline| (underline.order, batch_kind) < max_order_and_kind) .is_some() { underlines_end += 1; @@ -374,7 +374,8 @@ impl<'a> Iterator for BatchIterator<'a> { while self .monochrome_sprites_iter .next_if(|sprite| { - sprite.order < max_order && sprite.tile.texture_id == texture_id + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id }) .is_some() { @@ -394,7 +395,8 @@ impl<'a> Iterator for BatchIterator<'a> { while self .polychrome_sprites_iter .next_if(|sprite| { - sprite.order < max_order && sprite.tile.texture_id == texture_id + (sprite.order, batch_kind) < max_order_and_kind + && sprite.tile.texture_id == texture_id }) .is_some() { @@ -412,7 +414,7 @@ impl<'a> Iterator for BatchIterator<'a> { self.surfaces_iter.next(); while self .surfaces_iter - .next_if(|surface| surface.order < max_order) + .next_if(|surface| (surface.order, batch_kind) < max_order_and_kind) .is_some() { surfaces_end += 1; From 51127460b2ad235e8173d2e20f475a18a36f127f Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Wed, 17 Jan 2024 15:01:32 +0100 Subject: [PATCH 26/96] Remove memmove to improve terminal performance Co-authored-by: Antonio --- crates/terminal_view/src/terminal_element.rs | 109 +++++++++---------- 1 file changed, 53 insertions(+), 56 deletions(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1fec041de9c633493993696cdc6fdda142355c77..6298b4c16a07b47054430a6e2dcacd9e4f6e033e 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -250,8 +250,8 @@ impl TerminalElement { //Layout current cell text { - let cell_text = cell.c.to_string(); if !is_blank(&cell) { + let cell_text = cell.c.to_string(); let cell_style = TerminalElement::cell_style(&cell, fg, theme, text_style, hyperlink); @@ -586,24 +586,6 @@ impl TerminalElement { } } - fn register_key_listeners(&self, cx: &mut WindowContext) { - cx.on_key_event({ - let this = self.terminal.clone(); - move |event: &ModifiersChangedEvent, phase, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - let handled = - this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); - - if handled { - cx.refresh(); - } - } - }); - } - fn register_mouse_listeners( &mut self, origin: Point, @@ -771,53 +753,68 @@ impl Element for TerminalElement { self.register_mouse_listeners(origin, layout.mode, bounds, cx); - let mut interactivity = mem::take(&mut self.interactivity); - interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| { - cx.handle_input(&self.focus, terminal_input_handler); + self.interactivity + .paint(bounds, bounds.size, state, cx, |_, _, cx| { + cx.handle_input(&self.focus, terminal_input_handler); - self.register_key_listeners(cx); + cx.on_key_event({ + let this = self.terminal.clone(); + move |event: &ModifiersChangedEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } - for rect in &layout.rects { - rect.paint(origin, &layout, cx); - } + let handled = + this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); - cx.with_z_index(1, |cx| { - for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter() - { - if let Some((start_y, highlighted_range_lines)) = - to_highlighted_range_lines(relative_highlighted_range, &layout, origin) - { - let hr = HighlightedRange { - start_y, //Need to change this - line_height: layout.dimensions.line_height, - lines: highlighted_range_lines, - color: color.clone(), - //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.dimensions.line_height, - }; - hr.paint(bounds, cx); + if handled { + cx.refresh(); + } } - } - }); + }); - cx.with_z_index(2, |cx| { - for cell in &layout.cells { - cell.paint(origin, &layout, bounds, cx); + for rect in &layout.rects { + rect.paint(origin, &layout, cx); } - }); - if self.cursor_visible { - cx.with_z_index(3, |cx| { - if let Some(cursor) = &layout.cursor { - cursor.paint(origin, cx); + cx.with_z_index(1, |cx| { + for (relative_highlighted_range, color) in + layout.relative_highlighted_ranges.iter() + { + if let Some((start_y, highlighted_range_lines)) = + to_highlighted_range_lines(relative_highlighted_range, &layout, origin) + { + let hr = HighlightedRange { + start_y, //Need to change this + line_height: layout.dimensions.line_height, + lines: highlighted_range_lines, + color: color.clone(), + //Copied from editor. TODO: move to theme or something + corner_radius: 0.15 * layout.dimensions.line_height, + }; + hr.paint(bounds, cx); + } } }); - } - if let Some(mut element) = layout.hyperlink_tooltip.take() { - element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx) - } - }); + cx.with_z_index(2, |cx| { + for cell in &layout.cells { + cell.paint(origin, &layout, bounds, cx); + } + }); + + if self.cursor_visible { + cx.with_z_index(3, |cx| { + if let Some(cursor) = &layout.cursor { + cursor.paint(origin, cx); + } + }); + } + + if let Some(mut element) = layout.hyperlink_tooltip.take() { + element.draw(origin, bounds.size.map(AvailableSpace::Definite), cx) + } + }); } } From 977832a04ea03b2de94a6bb979e43f548cce46fc Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 17 Jan 2024 09:40:16 -0500 Subject: [PATCH 27/96] Refresh window, bypassing view cache, when opening hover or context menu --- crates/editor/src/hover_popover.rs | 1 + crates/ui/src/components/context_menu.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 8da2f50c198a01136d1318922a5159e3814a894f..609c20ac680819ff738f4a4f3ab08ffa58762146 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -339,6 +339,7 @@ fn show_hover( this.hover_state.info_popover = hover_popover; cx.notify(); + cx.refresh(); })?; Ok::<_, anyhow::Error>(()) diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 5c4f110a415b2e1e8ef0ac2d36b79d7d8bc2b4be..4b6837799976579b6bc469753c51cb964e49e7b0 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -51,6 +51,7 @@ impl ContextMenu { let _on_blur_subscription = cx.on_blur(&focus_handle, |this: &mut ContextMenu, cx| { this.cancel(&menu::Cancel, cx) }); + cx.refresh(); f( Self { items: Default::default(), From a601e96b6cf4e2612c7903d7ae4ff7b7c2d0faa0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 Jan 2024 16:44:43 +0200 Subject: [PATCH 28/96] Style collab notifications properly --- crates/workspace/src/pane_group.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index b6e3b1a433d358db2856e060f140be0654c06baf..e631cd9c436b6918a974d2afdcc7ac9d4aa37520 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -241,8 +241,9 @@ impl Member { .w_96() .bottom_3() .right_3() + .elevation_2(cx) + .p_1() .z_index(1) - .bg(cx.theme().colors().element_background) .child(status_box) .when_some( leader_join_data, From 9c557aae9e6963b30748d25852d8a04c03521405 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 11:00:59 -0500 Subject: [PATCH 29/96] Fix regression of welcome screen background color (#4091) In #3910 we made the welcome screen use the same background color as the editor. However, this later regressed in cdd5cb16ed896b2ee3bbb041983ee7cb812f6991. This PR fixes that regression and restores the correct color for the welcome page. Release Notes: - Fixed the background color of the welcome screen. --- crates/welcome/src/welcome.rs | 361 +++++++++++++++++----------------- 1 file changed, 184 insertions(+), 177 deletions(-) diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 677b57a0225059430b141ca23573d42433f52b77..53b78b917fec1d43c8c194a1b1bf0ee8198fe2a5 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -60,190 +60,197 @@ pub struct WelcomePage { impl Render for WelcomePage { fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - h_flex().full().track_focus(&self.focus_handle).child( - v_flex() - .w_96() - .gap_4() - .mx_auto() - .child( - svg() - .path("icons/logo_96.svg") - .text_color(gpui::white()) - .w(px(96.)) - .h(px(96.)) - .mx_auto(), - ) - .child( - h_flex() - .justify_center() - .child(Label::new("Code at the speed of thought")), - ) - .child( - v_flex() - .gap_2() - .child( - Button::new("choose-theme", "Choose a theme") - .full_width() - .on_click(cx.listener(|this, _, cx| { - this.telemetry - .report_app_event("welcome page: change theme".to_string()); - this.workspace - .update(cx, |workspace, cx| { - theme_selector::toggle( - workspace, - &Default::default(), - cx, - ) - }) - .ok(); - })), - ) - .child( - Button::new("choose-keymap", "Choose a keymap") - .full_width() - .on_click(cx.listener(|this, _, cx| { - this.telemetry.report_app_event( - "welcome page: change keymap".to_string(), - ); - this.workspace - .update(cx, |workspace, cx| { - base_keymap_picker::toggle( - workspace, - &Default::default(), - cx, - ) - }) - .ok(); - })), - ) - .child( - Button::new("install-cli", "Install the CLI") - .full_width() - .on_click(cx.listener(|this, _, cx| { - this.telemetry - .report_app_event("welcome page: install cli".to_string()); - cx.app_mut() - .spawn( - |cx| async move { install_cli::install_cli(&cx).await }, + h_flex() + .full() + .bg(cx.theme().colors().editor_background) + .track_focus(&self.focus_handle) + .child( + v_flex() + .w_96() + .gap_4() + .mx_auto() + .child( + svg() + .path("icons/logo_96.svg") + .text_color(gpui::white()) + .w(px(96.)) + .h(px(96.)) + .mx_auto(), + ) + .child( + h_flex() + .justify_center() + .child(Label::new("Code at the speed of thought")), + ) + .child( + v_flex() + .gap_2() + .child( + Button::new("choose-theme", "Choose a theme") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.telemetry.report_app_event( + "welcome page: change theme".to_string(), + ); + this.workspace + .update(cx, |workspace, cx| { + theme_selector::toggle( + workspace, + &Default::default(), + cx, + ) + }) + .ok(); + })), + ) + .child( + Button::new("choose-keymap", "Choose a keymap") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.telemetry.report_app_event( + "welcome page: change keymap".to_string(), + ); + this.workspace + .update(cx, |workspace, cx| { + base_keymap_picker::toggle( + workspace, + &Default::default(), + cx, + ) + }) + .ok(); + })), + ) + .child( + Button::new("install-cli", "Install the CLI") + .full_width() + .on_click(cx.listener(|this, _, cx| { + this.telemetry.report_app_event( + "welcome page: install cli".to_string(), + ); + cx.app_mut() + .spawn(|cx| async move { + install_cli::install_cli(&cx).await + }) + .detach_and_log_err(cx); + })), + ), + ) + .child( + v_flex() + .p_3() + .gap_2() + .bg(cx.theme().colors().elevated_surface_background) + .border_1() + .border_color(cx.theme().colors().border) + .rounded_md() + .child( + h_flex() + .gap_2() + .child( + Checkbox::new( + "enable-vim", + if VimModeSetting::get_global(cx).0 { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, ) - .detach_and_log_err(cx); - })), - ), - ) - .child( - v_flex() - .p_3() - .gap_2() - .bg(cx.theme().colors().elevated_surface_background) - .border_1() - .border_color(cx.theme().colors().border) - .rounded_md() - .child( - h_flex() - .gap_2() - .child( - Checkbox::new( - "enable-vim", - if VimModeSetting::get_global(cx).0 { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, + .on_click( + cx.listener(move |this, selection, cx| { + this.telemetry.report_app_event( + "welcome page: toggle vim".to_string(), + ); + this.update_settings::( + selection, + cx, + |setting, value| *setting = Some(value), + ); + }), + ), ) - .on_click(cx.listener( - move |this, selection, cx| { - this.telemetry.report_app_event( - "welcome page: toggle vim".to_string(), - ); - this.update_settings::( - selection, - cx, - |setting, value| *setting = Some(value), - ); - }, - )), - ) - .child(Label::new("Enable vim mode")), - ) - .child( - h_flex() - .gap_2() - .child( - Checkbox::new( - "enable-telemetry", - if TelemetrySettings::get_global(cx).metrics { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, - ) - .on_click(cx.listener( - move |this, selection, cx| { - this.telemetry.report_app_event( - "welcome page: toggle metric telemetry".to_string(), - ); - this.update_settings::( - selection, - cx, - { - let telemetry = this.telemetry.clone(); + .child(Label::new("Enable vim mode")), + ) + .child( + h_flex() + .gap_2() + .child( + Checkbox::new( + "enable-telemetry", + if TelemetrySettings::get_global(cx).metrics { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, + ) + .on_click( + cx.listener(move |this, selection, cx| { + this.telemetry.report_app_event( + "welcome page: toggle metric telemetry" + .to_string(), + ); + this.update_settings::( + selection, + cx, + { + let telemetry = this.telemetry.clone(); - move |settings, value| { - settings.metrics = Some(value); + move |settings, value| { + settings.metrics = Some(value); - telemetry.report_setting_event( - "metric telemetry", - value.to_string(), - ); - } - }, - ); - }, - )), - ) - .child(Label::new("Send anonymous usage data")), - ) - .child( - h_flex() - .gap_2() - .child( - Checkbox::new( - "enable-crash", - if TelemetrySettings::get_global(cx).diagnostics { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, + telemetry.report_setting_event( + "metric telemetry", + value.to_string(), + ); + } + }, + ); + }), + ), ) - .on_click(cx.listener( - move |this, selection, cx| { - this.telemetry.report_app_event( - "welcome page: toggle diagnostic telemetry" - .to_string(), - ); - this.update_settings::( - selection, - cx, - { - let telemetry = this.telemetry.clone(); + .child(Label::new("Send anonymous usage data")), + ) + .child( + h_flex() + .gap_2() + .child( + Checkbox::new( + "enable-crash", + if TelemetrySettings::get_global(cx).diagnostics { + ui::Selection::Selected + } else { + ui::Selection::Unselected + }, + ) + .on_click( + cx.listener(move |this, selection, cx| { + this.telemetry.report_app_event( + "welcome page: toggle diagnostic telemetry" + .to_string(), + ); + this.update_settings::( + selection, + cx, + { + let telemetry = this.telemetry.clone(); - move |settings, value| { - settings.diagnostics = Some(value); + move |settings, value| { + settings.diagnostics = Some(value); - telemetry.report_setting_event( - "diagnostic telemetry", - value.to_string(), - ); - } - }, - ); - }, - )), - ) - .child(Label::new("Send crash reports")), - ), - ), - ) + telemetry.report_setting_event( + "diagnostic telemetry", + value.to_string(), + ); + } + }, + ); + }), + ), + ) + .child(Label::new("Send crash reports")), + ), + ), + ) } } From 4cdcac1b16da71692802cf72c1b2abab41f5e58e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 11:39:09 -0500 Subject: [PATCH 30/96] Update docs --- crates/color/src/color.rs | 93 ++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs index d3d832099af16d85f2afd0e5e6551bca966618c6..8529f3bc5feea6b3248875e412914dc24b39a4b5 100644 --- a/crates/color/src/color.rs +++ b/crates/color/src/color.rs @@ -4,25 +4,49 @@ //! //! It is used to create a manipulate colors when building themes. //! -//! **Note:** This crate does not depend on `gpui`, so it does not provide any -//! interfaces for converting to `gpui` style colors. - +//! === In development note === +//! +//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff. +//! +//! It could be folded into gpui, ui or theme potentially but for now we'll continue +//! to develop it in isolation. +//! +//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths: +//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system. +//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed. +//! 3. Build the needed functionality into gpui and keep using it's color system everywhere. +//! +//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more. +//! +//! === End development note === use palette::{ blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha, }; +/// The types of blend modes supported #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum BlendMode { + /// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows. Multiply, + /// Lightens the color by adding the source and destination colors. It results in a lighter color. Screen, + /// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened. Overlay, + /// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast. Darken, + /// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast. Lighten, + /// Brightens the base color to reflect the blend color. The result is a lightened image. Dodge, + /// Darkens the base color to reflect the blend color. The result is a darkened image. Burn, + /// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color. HardLight, + /// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color. SoftLight, + /// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors. Difference, + /// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity. Exclusion, } @@ -30,7 +54,7 @@ pub enum BlendMode { /// /// This function supports the following hex formats: /// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`. -pub fn hex_to_hsla(s: &str) -> Result { +pub fn hex_to_hsla(s: &str) -> Result { let hex = s.trim_start_matches('#'); // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA @@ -61,16 +85,15 @@ pub fn hex_to_hsla(s: &str) -> Result { let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0; let a = (hex_val & 0xFF) as f32 / 255.0; - let color = Color { r, g, b, a }; + let color = RGBAColor { r, g, b, a }; Ok(color) } -// This implements conversion to and from all Palette colors. +// These derives implement to and from palette's color types. #[derive(FromColorUnclamped, WithAlpha, Debug, Clone)] -// We have to tell Palette that we will take care of converting to/from sRGB. #[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")] -pub struct Color { +pub struct RGBAColor { r: f32, g: f32, b: f32, @@ -79,22 +102,19 @@ pub struct Color { a: f32, } -// There's no blanket implementation for Self -> Self, unlike the From trait. -// This is to better allow cases like Self -> Self. -impl FromColorUnclamped for Color { - fn from_color_unclamped(color: Color) -> Color { +impl FromColorUnclamped for RGBAColor { + fn from_color_unclamped(color: RGBAColor) -> RGBAColor { color } } -// Convert from any kind of f32 sRGB. -impl FromColorUnclamped> for Color +impl FromColorUnclamped> for RGBAColor where Srgb: FromColorUnclamped>, { - fn from_color_unclamped(color: Rgb) -> Color { + fn from_color_unclamped(color: Rgb) -> RGBAColor { let srgb = Srgb::from_color_unclamped(color); - Color { + RGBAColor { r: srgb.red, g: srgb.green, b: srgb.blue, @@ -103,21 +123,19 @@ where } } -// Convert into any kind of f32 sRGB. -impl FromColorUnclamped for Rgb +impl FromColorUnclamped for Rgb where Rgb: FromColorUnclamped, { - fn from_color_unclamped(color: Color) -> Self { + fn from_color_unclamped(color: RGBAColor) -> Self { let srgb = Srgb::new(color.r, color.g, color.b); Self::from_color_unclamped(srgb) } } -// Add the required clamping. -impl Clamp for Color { +impl Clamp for RGBAColor { fn clamp(self) -> Self { - Color { + RGBAColor { r: self.r.min(1.0).max(0.0), g: self.g.min(1.0).max(0.0), b: self.b.min(1.0).max(0.0), @@ -126,9 +144,12 @@ impl Clamp for Color { } } -impl Color { +impl RGBAColor { + /// Creates a new color from the given RGBA values. + /// + /// This color can be used to convert to any [`palette::Color`] type. pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { - Color { r, g, b, a } + RGBAColor { r, g, b, a } } /// Returns a set of states for this color. @@ -137,16 +158,16 @@ impl Color { } /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`. - pub fn mixed(&self, other: Color, mix_ratio: f32) -> Self { + pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self { let srgb_self = Srgb::new(self.r, self.g, self.b); let srgb_other = Srgb::new(other.r, other.g, other.b); // Directly mix the colors as sRGB values let mixed = srgb_self.mix(srgb_other, mix_ratio); - Color::from_color_unclamped(mixed) + RGBAColor::from_color_unclamped(mixed) } - pub fn blend(&self, other: Color, blend_mode: BlendMode) -> Self { + pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self { let srgb_self = Srgb::new(self.r, self.g, self.b); let srgb_other = Srgb::new(other.r, other.g, other.b); @@ -169,31 +190,31 @@ impl Color { #[derive(Debug, Clone)] pub struct ColorStates { /// The default color. - pub default: Color, + pub default: RGBAColor, /// The color when the mouse is hovering over the element. - pub hover: Color, + pub hover: RGBAColor, /// The color when the mouse button is held down on the element. - pub active: Color, + pub active: RGBAColor, /// The color when the element is focused with the keyboard. - pub focused: Color, + pub focused: RGBAColor, /// The color when the element is disabled. - pub disabled: Color, + pub disabled: RGBAColor, } /// Returns a set of colors for different states of an element. /// -/// todo!("Test and improve this function") -pub fn states_for_color(color: Color, is_light: bool) -> ColorStates { +/// todo!("This should take a theme and use appropriate colors from it") +pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates { let adjustment_factor = if is_light { 0.1 } else { -0.1 }; let hover_adjustment = 1.0 - adjustment_factor; let active_adjustment = 1.0 - 2.0 * adjustment_factor; let focused_adjustment = 1.0 - 3.0 * adjustment_factor; let disabled_adjustment = 1.0 - 4.0 * adjustment_factor; - let make_adjustment = |color: Color, adjustment: f32| -> Color { + let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor { // Adjust lightness for each state // Note: Adjustment logic may differ; simplify as needed for sRGB - Color::new( + RGBAColor::new( color.r * adjustment, color.g * adjustment, color.b * adjustment, From df67917768b4fef7e6a30472ecba4403622be8ce Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 11:47:43 -0500 Subject: [PATCH 31/96] Make channel buttons square (#4092) This PR makes the channel buttons square. Release Notes: - Adjusted the shape of the channel buttons. --- crates/collab_ui/src/collab_panel.rs | 4 +-- .../ui/src/components/button/icon_button.rs | 17 +++++++--- .../ui/src/components/stories/icon_button.rs | 31 +++++++++++++++++-- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 47c5463e921eee93bfc4d1032dd74808ffb21fe7..d6de5135711b7e56854eaa541afc6b23a3020544 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2314,7 +2314,7 @@ impl CollabPanel { .child( IconButton::new("channel_chat", IconName::MessageBubbles) .style(ButtonStyle::Filled) - .size(ButtonSize::Compact) + .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(if has_messages_notification { Color::Default @@ -2332,7 +2332,7 @@ impl CollabPanel { .child( IconButton::new("channel_notes", IconName::File) .style(ButtonStyle::Filled) - .size(ButtonSize::Compact) + .shape(ui::IconButtonShape::Square) .icon_size(IconSize::Small) .icon_color(if has_notes_notification { Color::Default diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 1e37a872922b4473616b551352f8100bbd9b1327..cc1e31b65cb0836be9a307107302ba8705597d6c 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -127,16 +127,25 @@ impl VisibleOnHover for IconButton { } impl RenderOnce for IconButton { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { let is_disabled = self.base.disabled; let is_selected = self.base.selected; let selected_style = self.base.selected_style; self.base .map(|this| match self.shape { - IconButtonShape::Square => this - .width(self.icon_size.rems().into()) - .height(self.icon_size.rems().into()), + IconButtonShape::Square => { + let icon_size = self.icon_size.rems() * cx.rem_size(); + let padding = match self.icon_size { + IconSize::Indicator => px(0.), + IconSize::XSmall => px(0.), + IconSize::Small => px(2.), + IconSize::Medium => px(2.), + }; + + this.width((icon_size + padding * 2.).into()) + .height((icon_size + padding * 2.).into()) + } IconButtonShape::Wide => this, }) .child( diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index df9f37b164782f35c9a2ca1cfba6aa96d8783d60..ba3d5fd8660988298a38307cb8a690d1a365c7f4 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -1,7 +1,7 @@ use gpui::Render; use story::{StoryContainer, StoryItem, StorySection}; -use crate::{prelude::*, Tooltip}; +use crate::{prelude::*, IconButtonShape, Tooltip}; use crate::{IconButton, IconName}; pub struct IconButtonStory; @@ -115,7 +115,34 @@ impl Render for IconButtonStory { "Icon Button", "crates/ui2/src/components/stories/icon_button.rs", ) - .children(vec![StorySection::new().children(buttons)]) + .child(StorySection::new().children(buttons)) + .child( + StorySection::new().child(StoryItem::new( + "Square", + h_flex() + .gap_2() + .child( + IconButton::new("square-medium", IconName::Close) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Medium), + ) + .child( + IconButton::new("square-small", IconName::Close) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Small), + ) + .child( + IconButton::new("square-xsmall", IconName::Close) + .shape(IconButtonShape::Square) + .icon_size(IconSize::XSmall), + ) + .child( + IconButton::new("square-indicator", IconName::Close) + .shape(IconButtonShape::Square) + .icon_size(IconSize::Indicator), + ), + )), + ) .into_element() } } From 904695e4825b264082c8edcd322d349dcc67da08 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 12:32:08 -0500 Subject: [PATCH 32/96] Refine MVP CONTRIBUTING.md Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- CONTRIBUTING.md | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 825ddce79d2611bf1e48f4e2fcc11bdc98beeb6b..0c45475e4ca8ccbfbd1b22b9bc6270ca34c28cd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,16 +4,18 @@ Thanks for your interest in contributing to Zed, the collaborative platform that We want to ensure that no one ends up spending time on a pull request that may not be accepted, so we ask that you discuss your ideas with the team and community before starting on a contribution. -All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Contributors to Zed must sign our [Contributor License Agreement TODO](LINK) before their contributions can be merged. +All activity in Zed communities is subject to our [Code of Conduct](https://docs.zed.dev/community/code-of-conduct). Contributors to Zed must sign our Contributor License Agreement (link coming soon) before their contributions can be merged. ## Contribution ideas If you already have an idea of what you'd like to contribute, you can skip this section, otherwise, here are a few resources to help you find something to work on: -- Our [public roadmap TODO](LINK) details what features we plan to add to Zed. +- Our public roadmap (link coming soon!) details what features we plan to add to Zed. - Our [Top-Ranking Issues issue](https://github.com/zed-industries/community/issues/52) shows the most popular feature requests and issues, as voted on by the community. -At the moment, we are generally not looking to extend Zed's language or theme support by directly adding these features to Zed - we really want to build a plugin system to handle making the editor extensible going forward. This isn't to say that we won't accept contributions that add support for a new language or theme, but more to emphasize that we want to discuss these types of contributions first. +At the moment, we are generally not looking to extend Zed's language or theme support by directly adding these features to Zed - we really want to build a plugin system to handle making the editor extensible going forward. + +If you are passionate about shipping new languages or themes we suggest contributing to the extension system to help us get there faster. ## Resources @@ -30,31 +32,26 @@ Zed is made up of several smaller crates - let's go over those you're most likel - [language](/crates/language) drives `editor`'s understanding of language - from providing a list of symbols to the syntax map. - [collab](/crates/collab) is the collaboration server itself, driving the collaboration features such as project sharing. - [rpc](/crates/rpc) defines messages to be exchanged with collaboration server. +- [theme](/crates/theme) defines the theme system and provides a default theme. +- [ui](/crates/ui) is a collection of UI components and common patterns used throughout Zed. -## Zed channels - -Once you have an idea of what you'd like to contribute, you'll want to communicate this to the team. If you're new to Zed's channels, here's a guide [link to up-to-date docs TODO](LINK) to help bring you up to speed. - -[Since ~February 2022, the Zed Industries team has been exclusively using Zed to build Zed](https://x.com/nathansobo/status/1497958891509932035). We've built these tools to specifically address our own issues and frustrations with the current state of collaborative coding. These are not features we've built to simply look flashy, we work in channels every day of the workweek, aggressively dogfooding everything. - -![Staff usage of channels](./assets/screenshots/staff_usage_of_channels.png) - -*Channel metrics were not collected prior to August 2023* +### Proposal & Discussion -While we still have improvements to make, we believe we've sanded down a lot of the sharp edges and that the experience is both smooth and enjoyable - one that gets you as close to hypothetically sitting next to your teammates as possible, even if you're potentially on different sides of the globe. We want to continue working this way amongst ourselves and we are extremely excited to work with *you* in this way. We invite you to contribute to Zed *through* Zed. +Before starting on a contribution, we ask that you look to see if there is any existing PRs, or in-Zed discussions about the thing you want to implement. If there is no existing work, find a public channel that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can start a discussion, ask a team member or another contributor. -We plan to organize office hours on a weekly basis - they will take place in forelinked Zed channel. +*Please remember contributions not discussed with the team ahead of time likely have a lower chance of being merged or looked at in a timely manner.* -### Proposal & Discussion +## Implementation & Help -Before starting on a contribution, we ask that you look to see if there is any existing PRs, or in-Zed discussions about the thing you want to implement. If there is no existing work, find a [public channel TODO](LINK) that is relevant to your contribution, check the channel notes to see which Zed team members typically work in that channel, and post a message in the chat. If you're not sure which channel is best, you can post in the channel. +When you start working on your contribution if you find you are struggling with something specific feel free to reach out to the team for help. -*Please wait to begin working on your contribution until you've received feedback from the team. Turning down a contribution that was not discussed beforehand is a bummer for everyone.* +Remember the team is more likely to be available to help if you have already discussed your contribution or are working on something that is higher priority, like something on the roadmap or a top-ranking issue. -## Implementation & Help +We're happy to pair with you to help you learn the codebase and get your contribution merged. -Once approved, feel free to begin working on your contribution. If you have any questions, you can post in the channel you originally proposed your contribution in, or you can post in the channel. If you need help, reach out to a Zed teammate - we're happy to pair with you to help you learn the codebase and get your contribution merged. +**Zed makes heavy use of unit and integration testing, it is highly likely that contributions without any unit tests will be rejected** -**Zed makes heavy use of unit and integration testing, we encourage you to write tests for your contribution.** +Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. +We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. -Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. +Remeber that smaller, incremental PRs are easier to review and merge than large PRs. From 9415f098d773f3292fbc8dcbef715d97fb5f2971 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 13:09:02 -0500 Subject: [PATCH 33/96] Clean out old readme contents Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- README.md | 109 +++++++----------------------------------------------- 1 file changed, 13 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index eed8dd4d91c249dfa4de57f47d79c6ad1e2749e9..c97f4f5134d82ee6f9aa84c72992233feb47e255 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,27 @@ -# Zed - -[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) - -Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true. - -## Development tips - -### Dependencies - -* Install Xcode from https://apps.apple.com/us/app/xcode/id497799835?mt=12, and accept the license: - ``` - sudo xcodebuild -license - ``` - -* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.) - ``` - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - brew install node rustup-init - rustup-init # follow the installation steps - ``` - -* Install postgres and configure the database - ``` - brew install postgresql@15 - brew services start postgresql@15 - psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres - psql -U postgres -c "CREATE DATABASE zed" - ``` - -* Install the `LiveKit` server, the `PostgREST` API server, and the `foreman` process supervisor: - - ``` - brew install livekit - brew install postgrest - brew install foreman - ``` - -* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: - - ``` - cd .. - git clone https://github.com/zed-industries/zed.dev - cd zed.dev && npm install - npm install -g vercel - ``` - -* Return to Zed project directory and Initialize submodules - - ``` - cd zed - git submodule update --init --recursive - ``` +# 🚧 TODO 🚧 -* Set up a local `zed` database and seed it with some initial users: +[ ] Add intro +[ ] Add link to contributing guide +[ ] Add barebones running zed from source instructions +[ ] Link out to further dev docs - [Create a personal GitHub token](https://github.com/settings/tokens/new) to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope). - Then delete that token. - - ``` - GITHUB_TOKEN=<$token> script/bootstrap - ``` - -* Now try running zed with collaboration disabled: - ``` - cargo run - ``` - -### Common errors - -* `xcrun: error: unable to find utility "metal", not a developer tool or in PATH` - * You need to install Xcode and then run: `xcode-select --switch /Applications/Xcode.app/Contents/Developer` - * (see https://github.com/gfx-rs/gfx/issues/2309) - -### Testing against locally-running servers - -Start the web and collab servers: - -``` -foreman start -``` +# Zed -If you want to run Zed pointed at the local servers, you can run: +[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) -``` -script/zed-local -``` +Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom]https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). -### Dump element JSON +## Developing Zed -If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way. +- [Building Zed](./docs/src/developing_zed__building_zed.md) +- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md) ### Licensing +License information for third party dependencies must be correctly provided for CI to pass. + We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following: - Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml. - Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`. - Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration). - - -### Wasm Plugins - -Zed has a Wasm-based plugin runtime which it currently uses to embed plugins. To compile Zed, you'll need to have the `wasm32-wasi` toolchain installed on your system. To install this toolchain, run: - -```bash -rustup target add wasm32-wasi -``` - -Plugins can be found in the `plugins` folder in the root. For more information about how plugins work, check the [Plugin Guide](./crates/plugin_runtime/README.md) in `crates/plugin_runtime/README.md`. From b64ae4df9c47507478360316ffd878e3151b2603 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 13:09:13 -0500 Subject: [PATCH 34/96] Update developing zed doc Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- docs/src/developing_zed__building_zed.md | 90 +++++++++++++++--------- 1 file changed, 56 insertions(+), 34 deletions(-) diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index 7606e369d05cf02226ea5b783f4ff8369a661be3..947858033065619fd8f4bbca512af96bc7f6830e 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -7,56 +7,77 @@ How to build Zed from source for the first time. -## Prerequisites +### Prerequisites + +🚧 TODO 🚧 Update for open source - Be added to the GitHub organization - Be added to the Vercel team +- Create a [Personal Access Token](https://github.com/settings/personal-access-tokens/new) on Github + - 🚧 TODO 🚧 What permissions are required? + - 🚧 TODO 🚧 What changes when repo isn't private? + - Go to https://github.com/settings/tokens and Generate new token + - GitHub currently provides two kinds of tokens: + - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected + Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories + - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos + - Keep the token in the browser tab/editor for the next two steps + +### Dependencies + +* Install [Rust](https://www.rust-lang.org/tools/install) + +* Install the [GitHub CLI](https://cli.github.com/), [Livekit] & [Foreman] + +```bash +brew install gh +brew install livekit +brew install foreman +``` + +* Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store + +* Install Xcode command line tools + +```bash +xcode-select --install +``` + +- If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` -## Process +* Install [Postgres](https://postgresapp.com) -Expect this to take 30min to an hour! Some of these steps will take quite a while based on your connection speed, and how long your first build will be. +* Install the wasm toolchain + +```bash +rustup target add wasm32-wasi +``` + +### Building Zed from Source -1. Install the [GitHub CLI](https://cli.github.com/): - - `brew install gh` 1. Clone the `zed` repo - - `gh repo clone zed-industries/zed` -1. Install Xcode from the macOS App Store -1. Install Xcode command line tools - - `xcode-select --install` - - If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` -1. Install [Postgres](https://postgresapp.com) -1. Install rust/rustup - - `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` -1. Install the wasm toolchain - - `rustup target add wasm32-wasi` -1. Install Livekit & Foreman - - `brew install livekit` - - `brew install foreman` -1. Generate an GitHub API Key - - Go to https://github.com/settings/tokens and Generate new token - - GitHub currently provides two kinds of tokens: - - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected - Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories - - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos - - Keep the token in the browser tab/editor for the next two steps -1. (Optional but reccomended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` -1. Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: + +```bash +gh repo clone zed-industries/zed +``` + +1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` +1. (🚧 TODO 🚧 - Will this be relevant for open source?) Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: ``` cd .. git clone https://github.com/zed-industries/zed.dev cd zed.dev && npm install - npm install -g vercel + pnpm install -g vercel ``` -1. Link your zed.dev project to Vercel +1. (🚧 TODO 🚧 - Will this be relevant for open source?) Link your zed.dev project to Vercel - `vercel link` - Select the `zed-industries` team. If you don't have this get someone on the team to add you to it. - Select the `zed.dev` project -1. Run `vercel pull` to pull down the environment variables and project info from Vercel +1. (🚧 TODO 🚧 - Will this be relevant for open source?) Run `vercel pull` to pull down the environment variables and project info from Vercel 1. Open Postgres.app 1. From `./path/to/zed/`: - Run: - `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` - - Replace `{yourGithubAPIToken}` with the API token you generated above. - You don't need to include the GITHUB_TOKEN if you exported it above. - Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). - If you get: @@ -82,11 +103,12 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil ## Troubleshooting -### `error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)` +**`error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`** - Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer` -### `xcrun: error: unable to find utility "metal", not a developer tool or in PATH` +**`xcrun: error: unable to find utility "metal", not a developer tool or in PATH`** + ### Seeding errors during `script/bootstrap` runs @@ -104,4 +126,4 @@ Same command ### If you experience errors that mention some dependency is using unstable features -Try `cargo clean` and `cargo build` +Try `cargo clean` and `cargo build`, From d0f22df1eb30d80b0117cca308b352bbf97444ef Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 13:20:27 -0500 Subject: [PATCH 35/96] Reorganize building zed doc Co-Authored-By: Joseph T. Lyons <19867440+JosephTLyons@users.noreply.github.com> --- docs/src/developing_zed__building_zed.md | 100 ++++++++++++----------- 1 file changed, 52 insertions(+), 48 deletions(-) diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index 947858033065619fd8f4bbca512af96bc7f6830e..a5270e2b2abc8f89e88e23d016e2e3a21a065efd 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -1,7 +1,7 @@ # Building Zed 🚧 TODO: -- [ ] Tidy up & update instructions + - [ ] Remove ZI-specific things - [ ] Rework any steps that currently require a ZI-specific account @@ -14,20 +14,20 @@ How to build Zed from source for the first time. - Be added to the GitHub organization - Be added to the Vercel team - Create a [Personal Access Token](https://github.com/settings/personal-access-tokens/new) on Github - - 🚧 TODO 🚧 What permissions are required? - - 🚧 TODO 🚧 What changes when repo isn't private? - - Go to https://github.com/settings/tokens and Generate new token - - GitHub currently provides two kinds of tokens: - - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected - Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories - - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos - - Keep the token in the browser tab/editor for the next two steps + - 🚧 TODO 🚧 What permissions are required? + - 🚧 TODO 🚧 What changes when repo isn't private? + - Go to https://github.com/settings/tokens and Generate new token + - GitHub currently provides two kinds of tokens: + - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected + Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories + - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos + - Keep the token in the browser tab/editor for the next two steps ### Dependencies -* Install [Rust](https://www.rust-lang.org/tools/install) +- Install [Rust](https://www.rust-lang.org/tools/install) -* Install the [GitHub CLI](https://cli.github.com/), [Livekit] & [Foreman] +- Install the [GitHub CLI](https://cli.github.com/), [Livekit](https://formulae.brew.sh/formula/livekit) & [Foreman](https://formulae.brew.sh/formula/foreman) ```bash brew install gh @@ -35,15 +35,15 @@ brew install livekit brew install foreman ``` -* Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store +- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store -* Install Xcode command line tools +- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) ```bash xcode-select --install ``` -- If xcode-select --print-path prints /Library/Developer/CommandLineTools… run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` +- If `xcode-select --print-path prints /Library/Developer/CommandLineTools…` run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` * Install [Postgres](https://postgresapp.com) @@ -63,43 +63,34 @@ gh repo clone zed-industries/zed 1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` 1. (🚧 TODO 🚧 - Will this be relevant for open source?) Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: - ``` - cd .. - git clone https://github.com/zed-industries/zed.dev - cd zed.dev && npm install - pnpm install -g vercel - ``` + +```bash +cd .. +git clone https://github.com/zed-industries/zed.dev +cd zed.dev && npm install +pnpm install -g vercel +``` + 1. (🚧 TODO 🚧 - Will this be relevant for open source?) Link your zed.dev project to Vercel - - `vercel link` - - Select the `zed-industries` team. If you don't have this get someone on the team to add you to it. - - Select the `zed.dev` project + +- `vercel link` +- Select the `zed-industries` team. If you don't have this get someone on the team to add you to it. +- Select the `zed.dev` project + 1. (🚧 TODO 🚧 - Will this be relevant for open source?) Run `vercel pull` to pull down the environment variables and project info from Vercel 1. Open Postgres.app -1. From `./path/to/zed/`: - - Run: - - `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` - - You don't need to include the GITHUB_TOKEN if you exported it above. - - Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). - - If you get: - - ```bash - Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)! - Please create a new installation in /opt/homebrew using one of the - "Alternative Installs" from: - https://docs.brew.sh/Installation - ``` - - In that case try: - - `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` - - If Homebrew is not in your PATH: - - Replace `{username}` with your home folder name (usually your login name) - - `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile` - - `eval "$(/opt/homebrew/bin/brew shellenv)"` +1. From `./path/to/zed/` run `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` + +- You don't need to include the GITHUB_TOKEN if you exported it above. +- Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). + 1. To run the Zed app: - - If you are working on zed: - - `cargo run` - - If you are just using the latest version, but not working on zed: - - `cargo run --release` - - If you need to run the collaboration server locally: - - `script/zed-local` + - If you are working on zed: + - `cargo run` + - If you are just using the latest version, but not working on zed: + - `cargo run --release` + - If you need to run the collaboration server locally: + - `script/zed-local` ## Troubleshooting @@ -109,8 +100,21 @@ gh repo clone zed-industries/zed **`xcrun: error: unable to find utility "metal", not a developer tool or in PATH`** +### `script/bootstrap` + +```bash +Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)! +Please create a new installation in /opt/homebrew using one of the +"Alternative Installs" from: +https://docs.brew.sh/Installation +``` + +- In that case try `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` -### Seeding errors during `script/bootstrap` runs +- If Homebrew is not in your PATH: + - Replace `{username}` with your home folder name (usually your login name) + - `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile` + - `eval "$(/opt/homebrew/bin/brew shellenv)"` ``` seeding database... From ed67363ea38e06f8fea4fd761439510fe31c5fff Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 17 Jan 2024 13:24:05 -0500 Subject: [PATCH 36/96] Update README.md FIx typos --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c97f4f5134d82ee6f9aa84c72992233feb47e255..737615623194a958f4d8b8d129dfab50475e2e8b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # 🚧 TODO 🚧 -[ ] Add intro -[ ] Add link to contributing guide -[ ] Add barebones running zed from source instructions -[ ] Link out to further dev docs +- [ ] Add intro +- [ ] Add link to contributing guide +- [ ] Add barebones running zed from source instructions +- [ ] Link out to further dev docs # Zed [![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](https://github.com/zed-industries/zed/actions/workflows/ci.yml) -Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom]https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). +Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter). ## Developing Zed From 4e4a1e0dd1196b814667bf235f4b931d8b936e0e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 13:32:38 -0500 Subject: [PATCH 37/96] Document the public interface of the `vim` crate (#4093) This PR documents the public interface of the `vim` crate. Release Notes: - N/A --------- Co-authored-by: Conrad --- crates/vim/src/mode_indicator.rs | 11 +++-------- crates/vim/src/vim.rs | 24 ++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 4cd8e6690092d0f5f18528373e14d91f2311b156..b669b16112874a89cd303499cc0d17eab6b99267 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -4,12 +4,14 @@ use workspace::{item::ItemHandle, ui::prelude::*, StatusItemView}; use crate::{state::Mode, Vim}; +/// The ModeIndicator displays the current mode in the status bar. pub struct ModeIndicator { - pub mode: Option, + pub(crate) mode: Option, _subscriptions: Vec, } impl ModeIndicator { + /// Construct a new mode indicator in this window. pub fn new(cx: &mut ViewContext) -> Self { let _subscriptions = vec![ cx.observe_global::(|this, cx| this.update_mode(cx)), @@ -37,13 +39,6 @@ impl ModeIndicator { self.mode = None; } } - - pub fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext) { - if self.mode != Some(mode) { - self.mode = Some(mode); - cx.notify(); - } - } } impl Render for ModeIndicator { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 3579bf36fe29a7276496a1bbefd95dabf6cc8bc2..e03efb0a64b4b55e92ed8d92d8837351ac573e0d 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1,3 +1,5 @@ +//! Vim support for Zed. + #[cfg(test)] mod test; @@ -38,12 +40,18 @@ use crate::state::ReplayableAction; /// Default: false pub struct VimModeSetting(pub bool); +/// An Action to Switch between modes #[derive(Clone, Deserialize, PartialEq)] pub struct SwitchMode(pub Mode); +/// PushOperator is used to put vim into a "minor" mode, +/// where it's waiting for a specific next set of keystrokes. +/// For example 'd' needs a motion to complete. #[derive(Clone, Deserialize, PartialEq)] pub struct PushOperator(pub Operator); +/// Number is used to manage vim's count. Pushing a digit +/// multiplis the current value by 10 and adds the digit. #[derive(Clone, Deserialize, PartialEq)] struct Number(usize); @@ -51,11 +59,13 @@ actions!( vim, [Tab, Enter, Object, InnerObject, FindForward, FindBackward] ); + // in the workspace namespace so it's not filtered out when vim is disabled. actions!(workspace, [ToggleVimMode]); impl_actions!(vim, [SwitchMode, PushOperator, Number]); +/// Initializes the `vim` crate. pub fn init(cx: &mut AppContext) { cx.set_global(Vim::default()); VimModeSetting::register(cx); @@ -119,6 +129,7 @@ fn register(workspace: &mut Workspace, cx: &mut ViewContext) { visual::register(workspace, cx); } +/// Registers a keystroke observer to observe keystrokes for the Vim integration. pub fn observe_keystrokes(cx: &mut WindowContext) { cx.observe_keystrokes(|keystroke_event, cx| { if let Some(action) = keystroke_event @@ -160,6 +171,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) { .detach() } +/// The state pertaining to Vim mode. Stored as a global. #[derive(Default)] pub struct Vim { active_editor: Option>, @@ -251,6 +263,8 @@ impl Vim { Some(editor.update(cx, update)) } + /// When doing an action that modifies the buffer, we start recording so that `.` + /// will replay the action. pub fn start_recording(&mut self, cx: &mut WindowContext) { if !self.workspace_state.replaying { self.workspace_state.recording = true; @@ -295,12 +309,19 @@ impl Vim { } } + /// When finishing an action that modifies the buffer, stop recording. + /// as you usually call this within a keystroke handler we also ensure that + /// the current action is recorded. pub fn stop_recording(&mut self) { if self.workspace_state.recording { self.workspace_state.stop_recording_after_next_action = true; } } + /// Stops recording actions immediately rather than waiting until after the + /// next action to stop recording. + /// + /// This doesn't include the current action. pub fn stop_recording_immediately(&mut self, action: Box) { if self.workspace_state.recording { self.workspace_state @@ -311,6 +332,7 @@ impl Vim { } } + /// Explicitly record one action (equiavlent to start_recording and stop_recording) pub fn record_current_action(&mut self, cx: &mut WindowContext) { self.start_recording(cx); self.stop_recording(); @@ -516,6 +538,7 @@ impl Vim { } } + /// Returns the state of the active editor. pub fn state(&self) -> &EditorState { if let Some(active_editor) = self.active_editor.as_ref() { if let Some(state) = self.editor_states.get(&active_editor.entity_id()) { @@ -526,6 +549,7 @@ impl Vim { &self.default_state } + /// Updates the state of the active editor. pub fn update_state(&mut self, func: impl FnOnce(&mut EditorState) -> T) -> T { let mut state = self.state().clone(); let ret = func(&mut state); From 6734e528a845300c94675431c8b1d0d4d9728c5c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Jan 2024 10:33:32 -0800 Subject: [PATCH 38/96] Revert "Bump livekit client" This reverts commit 5730d0ef2107f2a10fe01595ce34f14f97e289ad. --- crates/live_kit_client/LiveKitBridge/Package.resolved | 8 ++++---- crates/live_kit_client/LiveKitBridge/Package.swift | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/live_kit_client/LiveKitBridge/Package.resolved b/crates/live_kit_client/LiveKitBridge/Package.resolved index bf17ef24c57da952f27caee6c995af0800a73e2e..b925bc8f0d5ef290993fa0d49adcf221dd3570f6 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.resolved +++ b/crates/live_kit_client/LiveKitBridge/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/livekit/client-sdk-swift.git", "state": { "branch": null, - "revision": "8b9cefed8d1669ec8fce41376b56dce3036a5f50", - "version": "1.1.4" + "revision": "7331b813a5ab8a95cfb81fb2b4ed10519428b9ff", + "version": "1.0.12" } }, { @@ -24,8 +24,8 @@ "repositoryURL": "https://github.com/webrtc-sdk/Specs.git", "state": { "branch": null, - "revision": "4fa8d6d647fc759cdd0265fd413d2f28ea2e0e08", - "version": "114.5735.8" + "revision": "2f6bab30c8df0fe59ab3e58bc99097f757f85f65", + "version": "104.5112.17" } }, { diff --git a/crates/live_kit_client/LiveKitBridge/Package.swift b/crates/live_kit_client/LiveKitBridge/Package.swift index abb38efca6a734c935ecdd7438570a3fa6f94230..d7b5c271b95496112b2fc368a851506d54b176b8 100644 --- a/crates/live_kit_client/LiveKitBridge/Package.swift +++ b/crates/live_kit_client/LiveKitBridge/Package.swift @@ -15,7 +15,7 @@ let package = Package( targets: ["LiveKitBridge"]), ], dependencies: [ - .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.1.4")), + .package(url: "https://github.com/livekit/client-sdk-swift.git", .exact("1.0.12")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. From cf5dc099fb9282946372f067795ae0061b36c416 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 13:38:12 -0500 Subject: [PATCH 39/96] Add more documentation to `collab` (#4095) This PR adds more documentation to the `collab` crate. Release Notes: - N/A --------- Co-authored-by: Conrad --- crates/collab/src/auth.rs | 7 +++ crates/collab/src/db.rs | 26 ++++++++ crates/collab/src/db/ids.rs | 20 ++++++ crates/collab/src/db/queries/access_tokens.rs | 2 + crates/collab/src/db/queries/buffers.rs | 10 ++- crates/collab/src/db/queries/channels.rs | 28 +++++++-- crates/collab/src/db/queries/contacts.rs | 8 +++ crates/collab/src/db/queries/messages.rs | 11 ++++ crates/collab/src/db/queries/notifications.rs | 4 ++ crates/collab/src/db/queries/projects.rs | 18 ++++++ crates/collab/src/db/queries/rooms.rs | 6 ++ crates/collab/src/db/queries/servers.rs | 6 ++ crates/collab/src/db/queries/users.rs | 15 +++++ crates/collab/src/db/tables/user.rs | 1 + crates/collab/src/rpc.rs | 62 +++++++++++++++++++ 15 files changed, 219 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index 9ce602c5778c25efe89017b58b3498108de38038..df3ded28e4a2f1f86ac1421e91dc6be31f7f8408 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -27,6 +27,8 @@ lazy_static! { .unwrap(); } +/// Validates the authorization header. This has two mechanisms, one for the ADMIN_TOKEN +/// and one for the access tokens that we issue. pub async fn validate_header(mut req: Request, next: Next) -> impl IntoResponse { let mut auth_header = req .headers() @@ -88,6 +90,8 @@ struct AccessTokenJson { token: String, } +/// Creates a new access token to identify the given user. before returning it, you should +/// encrypt it with the user's public key. pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result { const VERSION: usize = 1; let access_token = rpc::auth::random_token(); @@ -122,6 +126,8 @@ fn hash_access_token(token: &str) -> Result { .to_string()) } +/// Encrypts the given access token with the given public key to avoid leaking it on the way +/// to the client. pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result { let native_app_public_key = rpc::auth::PublicKey::try_from(public_key).context("failed to parse app public key")?; @@ -131,6 +137,7 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result) -> Result { let token: AccessTokenJson = serde_json::from_str(&token)?; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index df33416a46b00cea970e05f22b48c08cc02230cf..480dcf6f8595143659069739104608dac4c15fd8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -47,6 +47,8 @@ pub use ids::*; pub use sea_orm::ConnectOptions; pub use tables::user::Model as User; +/// Database gives you a handle that lets you access the database. +/// It handles pooling internally. pub struct Database { options: ConnectOptions, pool: DatabaseConnection, @@ -62,6 +64,7 @@ pub struct Database { // The `Database` type has so many methods that its impl blocks are split into // separate files in the `queries` folder. impl Database { + /// Connects to the database with the given options pub async fn new(options: ConnectOptions, executor: Executor) -> Result { sqlx::any::install_default_drivers(); Ok(Self { @@ -82,6 +85,7 @@ impl Database { self.rooms.clear(); } + /// Runs the database migrations. pub async fn migrate( &self, migrations_path: &Path, @@ -123,11 +127,15 @@ impl Database { Ok(new_migrations) } + /// Initializes static data that resides in the database by upserting it. pub async fn initialize_static_data(&mut self) -> Result<()> { self.initialize_notification_kinds().await?; Ok(()) } + /// Transaction runs things in a transaction. If you want to call other methods + /// and pass the transaction around you need to reborrow the transaction at each + /// call site with: `&*tx`. pub async fn transaction(&self, f: F) -> Result where F: Send + Fn(TransactionHandle) -> Fut, @@ -160,6 +168,7 @@ impl Database { self.run(body).await } + /// The same as room_transaction, but if you need to only optionally return a Room. async fn optional_room_transaction(&self, f: F) -> Result>> where F: Send + Fn(TransactionHandle) -> Fut, @@ -210,6 +219,9 @@ impl Database { self.run(body).await } + /// room_transaction runs the block in a transaction. It returns a RoomGuard, that keeps + /// the database locked until it is dropped. This ensures that updates sent to clients are + /// properly serialized with respect to database changes. async fn room_transaction(&self, room_id: RoomId, f: F) -> Result> where F: Send + Fn(TransactionHandle) -> Fut, @@ -330,6 +342,7 @@ fn is_serialization_error(error: &Error) -> bool { } } +/// A handle to a [`DatabaseTransaction`]. pub struct TransactionHandle(Arc>); impl Deref for TransactionHandle { @@ -340,6 +353,8 @@ impl Deref for TransactionHandle { } } +/// [`RoomGuard`] keeps a database transaction alive until it is dropped. +/// so that updates to rooms are serialized. pub struct RoomGuard { data: T, _guard: OwnedMutexGuard<()>, @@ -361,6 +376,7 @@ impl DerefMut for RoomGuard { } impl RoomGuard { + /// Returns the inner value of the guard. pub fn into_inner(self) -> T { self.data } @@ -420,12 +436,14 @@ pub struct WaitlistSummary { pub unknown_count: i64, } +/// The parameters to create a new user. #[derive(Debug, Serialize, Deserialize)] pub struct NewUserParams { pub github_login: String, pub github_user_id: i32, } +/// The result of creating a new user. #[derive(Debug)] pub struct NewUserResult { pub user_id: UserId, @@ -434,6 +452,7 @@ pub struct NewUserResult { pub signup_device_id: Option, } +/// The result of moving a channel. #[derive(Debug)] pub struct MoveChannelResult { pub participants_to_update: HashMap, @@ -441,18 +460,21 @@ pub struct MoveChannelResult { pub moved_channels: HashSet, } +/// The result of renaming a channel. #[derive(Debug)] pub struct RenameChannelResult { pub channel: Channel, pub participants_to_update: HashMap, } +/// The result of creating a channel. #[derive(Debug)] pub struct CreateChannelResult { pub channel: Channel, pub participants_to_update: Vec<(UserId, ChannelsForUser)>, } +/// The result of setting a channel's visibility. #[derive(Debug)] pub struct SetChannelVisibilityResult { pub participants_to_update: HashMap, @@ -460,6 +482,7 @@ pub struct SetChannelVisibilityResult { pub channels_to_remove: Vec, } +/// The result of updating a channel membership. #[derive(Debug)] pub struct MembershipUpdated { pub channel_id: ChannelId, @@ -467,12 +490,14 @@ pub struct MembershipUpdated { pub removed_channels: Vec, } +/// The result of setting a member's role. #[derive(Debug)] pub enum SetMemberRoleResult { InviteUpdated(Channel), MembershipUpdated(MembershipUpdated), } +/// The result of inviting a member to a channel. #[derive(Debug)] pub struct InviteMemberResult { pub channel: Channel, @@ -497,6 +522,7 @@ pub struct Channel { pub name: String, pub visibility: ChannelVisibility, pub role: ChannelRole, + /// parent_path is the channel ids from the root to this one (not including this one) pub parent_path: Vec, } diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 9dbbfaff8f8d0e94ea56b02eda37485ddc695fa8..a920265b5703e55ef482a88c03facea95194265b 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -19,19 +19,23 @@ macro_rules! id_type { Deserialize, DeriveValueType, )] + #[allow(missing_docs)] #[serde(transparent)] pub struct $name(pub i32); impl $name { #[allow(unused)] + #[allow(missing_docs)] pub const MAX: Self = Self(i32::MAX); #[allow(unused)] + #[allow(missing_docs)] pub fn from_proto(value: u64) -> Self { Self(value as i32) } #[allow(unused)] + #[allow(missing_docs)] pub fn to_proto(self) -> u64 { self.0 as u64 } @@ -84,21 +88,28 @@ id_type!(FlagId); id_type!(NotificationId); id_type!(NotificationKindId); +/// ChannelRole gives you permissions for both channels and calls. #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum ChannelRole { + /// Admin can read/write and change permissions. #[sea_orm(string_value = "admin")] Admin, + /// Member can read/write, but not change pemissions. #[sea_orm(string_value = "member")] #[default] Member, + /// Guest can read, but not write. + /// (thought they can use the channel chat) #[sea_orm(string_value = "guest")] Guest, + /// Banned may not read. #[sea_orm(string_value = "banned")] Banned, } impl ChannelRole { + /// Returns true if this role is more powerful than the other role. pub fn should_override(&self, other: Self) -> bool { use ChannelRole::*; match self { @@ -109,6 +120,7 @@ impl ChannelRole { } } + /// Returns the maximal role between the two pub fn max(&self, other: Self) -> Self { if self.should_override(other) { *self @@ -117,6 +129,7 @@ impl ChannelRole { } } + /// True if the role allows access to all descendant channels pub fn can_see_all_descendants(&self) -> bool { use ChannelRole::*; match self { @@ -125,6 +138,7 @@ impl ChannelRole { } } + /// True if the role only allows access to public descendant channels pub fn can_only_see_public_descendants(&self) -> bool { use ChannelRole::*; match self { @@ -133,6 +147,7 @@ impl ChannelRole { } } + /// True if the role can share screen/microphone/projects into rooms. pub fn can_publish_to_rooms(&self) -> bool { use ChannelRole::*; match self { @@ -141,6 +156,7 @@ impl ChannelRole { } } + /// True if the role can edit shared projects. pub fn can_edit_projects(&self) -> bool { use ChannelRole::*; match self { @@ -149,6 +165,7 @@ impl ChannelRole { } } + /// True if the role can read shared projects. pub fn can_read_projects(&self) -> bool { use ChannelRole::*; match self { @@ -187,11 +204,14 @@ impl Into for ChannelRole { } } +/// ChannelVisibility controls whether channels are public or private. #[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)] #[sea_orm(rs_type = "String", db_type = "String(None)")] pub enum ChannelVisibility { + /// Public channels are visible to anyone with the link. People join with the Guest role by default. #[sea_orm(string_value = "public")] Public, + /// Members channels are only visible to members of this channel or its parents. #[sea_orm(string_value = "members")] #[default] Members, diff --git a/crates/collab/src/db/queries/access_tokens.rs b/crates/collab/src/db/queries/access_tokens.rs index 589b6483dfceb5df285ac67b03edbee493e4705b..e0338189708c41d63b168f37c89e2c62fe3521b9 100644 --- a/crates/collab/src/db/queries/access_tokens.rs +++ b/crates/collab/src/db/queries/access_tokens.rs @@ -2,6 +2,7 @@ use super::*; use sea_orm::sea_query::Query; impl Database { + /// Creates a new access token for the given user. pub async fn create_access_token( &self, user_id: UserId, @@ -39,6 +40,7 @@ impl Database { .await } + /// Retrieves the access token with the given ID. pub async fn get_access_token( &self, access_token_id: AccessTokenId, diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 9eddb1f6187a80f4f88f8d13e9aff3f4c310941f..85e71d753b972db0515954406819f8c2fc48f9a9 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -9,6 +9,8 @@ pub struct LeftChannelBuffer { } impl Database { + /// Open a channel buffer. Returns the current contents, and adds you to the list of people + /// to notify on changes. pub async fn join_channel_buffer( &self, channel_id: ChannelId, @@ -121,6 +123,7 @@ impl Database { .await } + /// Rejoin a channel buffer (after a connection interruption) pub async fn rejoin_channel_buffers( &self, buffers: &[proto::ChannelBufferVersion], @@ -232,6 +235,7 @@ impl Database { .await } + /// Clear out any buffer collaborators who are no longer collaborating. pub async fn clear_stale_channel_buffer_collaborators( &self, channel_id: ChannelId, @@ -274,6 +278,7 @@ impl Database { .await } + /// Close the channel buffer, and stop receiving updates for it. pub async fn leave_channel_buffer( &self, channel_id: ChannelId, @@ -286,6 +291,7 @@ impl Database { .await } + /// Close the channel buffer, and stop receiving updates for it. pub async fn channel_buffer_connection_lost( &self, connection: ConnectionId, @@ -309,6 +315,7 @@ impl Database { Ok(()) } + /// Close all open channel buffers pub async fn leave_channel_buffers( &self, connection: ConnectionId, @@ -342,7 +349,7 @@ impl Database { .await } - pub async fn leave_channel_buffer_internal( + async fn leave_channel_buffer_internal( &self, channel_id: ChannelId, connection: ConnectionId, @@ -798,6 +805,7 @@ impl Database { Ok(changes) } + /// Returns the latest operations for the buffers with the specified IDs. pub async fn get_latest_operations_for_buffers( &self, buffer_ids: impl IntoIterator, diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 6243b03bf7ad2f331b3ede34c2390db574940546..7ff9f00bc119c12a17987b2b7dff24e354286c33 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -40,6 +40,7 @@ impl Database { .id) } + /// Creates a new channel. pub async fn create_channel( &self, name: &str, @@ -97,6 +98,7 @@ impl Database { .await } + /// Adds a user to the specified channel. pub async fn join_channel( &self, channel_id: ChannelId, @@ -179,6 +181,7 @@ impl Database { .await } + /// Sets the visibiltity of the given channel. pub async fn set_channel_visibility( &self, channel_id: ChannelId, @@ -258,6 +261,7 @@ impl Database { .await } + /// Deletes the channel with the specified ID. pub async fn delete_channel( &self, channel_id: ChannelId, @@ -294,6 +298,7 @@ impl Database { .await } + /// Invites a user to a channel as a member. pub async fn invite_channel_member( &self, channel_id: ChannelId, @@ -349,6 +354,7 @@ impl Database { Ok(new_name) } + /// Renames the specified channel. pub async fn rename_channel( &self, channel_id: ChannelId, @@ -387,6 +393,7 @@ impl Database { .await } + /// accept or decline an invite to join a channel pub async fn respond_to_channel_invite( &self, channel_id: ChannelId, @@ -486,6 +493,7 @@ impl Database { }) } + /// Removes a channel member. pub async fn remove_channel_member( &self, channel_id: ChannelId, @@ -530,6 +538,7 @@ impl Database { .await } + /// Returns all channel invites for the user with the given ID. pub async fn get_channel_invites_for_user(&self, user_id: UserId) -> Result> { self.transaction(|tx| async move { let mut role_for_channel: HashMap = HashMap::default(); @@ -565,6 +574,7 @@ impl Database { .await } + /// Returns all channels for the user with the given ID. pub async fn get_channels_for_user(&self, user_id: UserId) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -574,6 +584,8 @@ impl Database { .await } + /// Returns all channels for the user with the given ID that are descendants + /// of the specified ancestor channel. pub async fn get_user_channels( &self, user_id: UserId, @@ -743,6 +755,7 @@ impl Database { Ok(results) } + /// Sets the role for the specified channel member. pub async fn set_channel_member_role( &self, channel_id: ChannelId, @@ -786,6 +799,7 @@ impl Database { .await } + /// Returns the details for the specified channel member. pub async fn get_channel_participant_details( &self, channel_id: ChannelId, @@ -911,6 +925,7 @@ impl Database { .collect()) } + /// Returns the participants in the given channel. pub async fn get_channel_participants( &self, channel: &channel::Model, @@ -925,6 +940,7 @@ impl Database { .collect()) } + /// Returns whether the given user is an admin in the specified channel. pub async fn check_user_is_channel_admin( &self, channel: &channel::Model, @@ -943,6 +959,7 @@ impl Database { } } + /// Returns whether the given user is a member of the specified channel. pub async fn check_user_is_channel_member( &self, channel: &channel::Model, @@ -958,6 +975,7 @@ impl Database { } } + /// Returns whether the given user is a participant in the specified channel. pub async fn check_user_is_channel_participant( &self, channel: &channel::Model, @@ -975,6 +993,7 @@ impl Database { } } + /// Returns a user's pending invite for the given channel, if one exists. pub async fn pending_invite_for_channel( &self, channel: &channel::Model, @@ -991,7 +1010,7 @@ impl Database { Ok(row) } - pub async fn public_parent_channel( + async fn public_parent_channel( &self, channel: &channel::Model, tx: &DatabaseTransaction, @@ -1003,7 +1022,7 @@ impl Database { Ok(path.pop()) } - pub async fn public_ancestors_including_self( + pub(crate) async fn public_ancestors_including_self( &self, channel: &channel::Model, tx: &DatabaseTransaction, @@ -1018,6 +1037,7 @@ impl Database { Ok(visible_channels) } + /// Returns the role for a user in the given channel. pub async fn channel_role_for_user( &self, channel: &channel::Model, @@ -1143,7 +1163,7 @@ impl Database { .await?) } - /// Returns the channel with the given ID + /// Returns the channel with the given ID. pub async fn get_channel(&self, channel_id: ChannelId, user_id: UserId) -> Result { self.transaction(|tx| async move { let channel = self.get_channel_internal(channel_id, &*tx).await?; @@ -1156,7 +1176,7 @@ impl Database { .await } - pub async fn get_channel_internal( + pub(crate) async fn get_channel_internal( &self, channel_id: ChannelId, tx: &DatabaseTransaction, diff --git a/crates/collab/src/db/queries/contacts.rs b/crates/collab/src/db/queries/contacts.rs index f31f1addbd2b4a210abfa9810f062585a7b656e4..c66c33b80dfd795c79f8ef002b0ee122954232cb 100644 --- a/crates/collab/src/db/queries/contacts.rs +++ b/crates/collab/src/db/queries/contacts.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Retrieves the contacts for the user with the given ID. pub async fn get_contacts(&self, user_id: UserId) -> Result> { #[derive(Debug, FromQueryResult)] struct ContactWithUserBusyStatuses { @@ -86,6 +87,7 @@ impl Database { .await } + /// Returns whether the given user is a busy (on a call). pub async fn is_user_busy(&self, user_id: UserId) -> Result { self.transaction(|tx| async move { let participant = room_participant::Entity::find() @@ -97,6 +99,9 @@ impl Database { .await } + /// Returns whether the user with `user_id_1` has the user with `user_id_2` as a contact. + /// + /// In order for this to return `true`, `user_id_2` must have an accepted invite from `user_id_1`. pub async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result { self.transaction(|tx| async move { let (id_a, id_b) = if user_id_1 < user_id_2 { @@ -119,6 +124,7 @@ impl Database { .await } + /// Invite the user with `receiver_id` to be a contact of the user with `sender_id`. pub async fn send_contact_request( &self, sender_id: UserId, @@ -231,6 +237,7 @@ impl Database { .await } + /// Dismisses a contact notification for the given user. pub async fn dismiss_contact_notification( &self, user_id: UserId, @@ -272,6 +279,7 @@ impl Database { .await } + /// Accept or decline a contact request pub async fn respond_to_contact_request( &self, responder_id: UserId, diff --git a/crates/collab/src/db/queries/messages.rs b/crates/collab/src/db/queries/messages.rs index 96942c052df75c9406272da3a563533342f64406..9ee313d91b4f916c69789b2048c1867be207f676 100644 --- a/crates/collab/src/db/queries/messages.rs +++ b/crates/collab/src/db/queries/messages.rs @@ -4,6 +4,7 @@ use sea_orm::TryInsertResult; use time::OffsetDateTime; impl Database { + /// Inserts a record representing a user joining the chat for a given channel. pub async fn join_channel_chat( &self, channel_id: ChannelId, @@ -28,6 +29,7 @@ impl Database { .await } + /// Removes `channel_chat_participant` records associated with the given connection ID. pub async fn channel_chat_connection_lost( &self, connection_id: ConnectionId, @@ -47,6 +49,8 @@ impl Database { Ok(()) } + /// Removes `channel_chat_participant` records associated with the given user ID so they + /// will no longer get chat notifications. pub async fn leave_channel_chat( &self, channel_id: ChannelId, @@ -72,6 +76,9 @@ impl Database { .await } + /// Retrieves the messages in the specified channel. + /// + /// Use `before_message_id` to paginate through the channel's messages. pub async fn get_channel_messages( &self, channel_id: ChannelId, @@ -103,6 +110,7 @@ impl Database { .await } + /// Returns the channel messages with the given IDs. pub async fn get_channel_messages_by_id( &self, user_id: UserId, @@ -190,6 +198,7 @@ impl Database { Ok(messages) } + /// Creates a new channel message. pub async fn create_channel_message( &self, channel_id: ChannelId, @@ -376,6 +385,7 @@ impl Database { Ok(()) } + /// Returns the unseen messages for the given user in the specified channels. pub async fn unseen_channel_messages( &self, user_id: UserId, @@ -449,6 +459,7 @@ impl Database { Ok(changes) } + /// Removes the channel message with the given ID. pub async fn remove_channel_message( &self, channel_id: ChannelId, diff --git a/crates/collab/src/db/queries/notifications.rs b/crates/collab/src/db/queries/notifications.rs index 6f2511c23e7cd383760aa29ec62a65ca30c636d8..57685e141b78e5aa6c4a541c385bf056b1802000 100644 --- a/crates/collab/src/db/queries/notifications.rs +++ b/crates/collab/src/db/queries/notifications.rs @@ -2,6 +2,7 @@ use super::*; use rpc::Notification; impl Database { + /// Initializes the different kinds of notifications by upserting records for them. pub async fn initialize_notification_kinds(&mut self) -> Result<()> { notification_kind::Entity::insert_many(Notification::all_variant_names().iter().map( |kind| notification_kind::ActiveModel { @@ -28,6 +29,7 @@ impl Database { Ok(()) } + /// Returns the notifications for the given recipient. pub async fn get_notifications( &self, recipient_id: UserId, @@ -140,6 +142,7 @@ impl Database { .await } + /// Marks the given notification as read. pub async fn mark_notification_as_read( &self, recipient_id: UserId, @@ -150,6 +153,7 @@ impl Database { .await } + /// Marks the notification with the given ID as read. pub async fn mark_notification_as_read_by_id( &self, recipient_id: UserId, diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index d82a597038d063405a22aeddd0623d7f9bfc9eb0..f81403a796b0e2402eef5c46124c8b51bc68d2b5 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Returns the count of all projects, excluding ones marked as admin. pub async fn project_count_excluding_admins(&self) -> Result { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryAs { @@ -21,6 +22,7 @@ impl Database { .await } + /// Shares a project with the given room. pub async fn share_project( &self, room_id: RoomId, @@ -100,6 +102,7 @@ impl Database { .await } + /// Unshares the given project. pub async fn unshare_project( &self, project_id: ProjectId, @@ -126,6 +129,7 @@ impl Database { .await } + /// Updates the worktrees associated with the given project. pub async fn update_project( &self, project_id: ProjectId, @@ -346,6 +350,7 @@ impl Database { .await } + /// Updates the diagnostic summary for the given connection. pub async fn update_diagnostic_summary( &self, update: &proto::UpdateDiagnosticSummary, @@ -401,6 +406,7 @@ impl Database { .await } + /// Starts the language server for the given connection. pub async fn start_language_server( &self, update: &proto::StartLanguageServer, @@ -447,6 +453,7 @@ impl Database { .await } + /// Updates the worktree settings for the given connection. pub async fn update_worktree_settings( &self, update: &proto::UpdateWorktreeSettings, @@ -499,6 +506,7 @@ impl Database { .await } + /// Adds the given connection to the specified project. pub async fn join_project( &self, project_id: ProjectId, @@ -704,6 +712,7 @@ impl Database { .await } + /// Removes the given connection from the specified project. pub async fn leave_project( &self, project_id: ProjectId, @@ -805,6 +814,7 @@ impl Database { .map(|guard| guard.into_inner()) } + /// Returns the host connection for a read-only request to join a shared project. pub async fn host_for_read_only_project_request( &self, project_id: ProjectId, @@ -842,6 +852,7 @@ impl Database { .map(|guard| guard.into_inner()) } + /// Returns the host connection for a request to join a shared project. pub async fn host_for_mutating_project_request( &self, project_id: ProjectId, @@ -927,6 +938,10 @@ impl Database { .await } + /// Returns the connection IDs in the given project. + /// + /// The provided `connection_id` must also be a collaborator in the project, + /// otherwise an error will be returned. pub async fn project_connection_ids( &self, project_id: ProjectId, @@ -976,6 +991,7 @@ impl Database { Ok(guest_connection_ids) } + /// Returns the [`RoomId`] for the given project. pub async fn room_id_for_project(&self, project_id: ProjectId) -> Result { self.transaction(|tx| async move { let project = project::Entity::find_by_id(project_id) @@ -1020,6 +1036,7 @@ impl Database { .await } + /// Adds the given follower connection as a follower of the given leader connection. pub async fn follow( &self, room_id: RoomId, @@ -1050,6 +1067,7 @@ impl Database { .await } + /// Removes the given follower connection as a follower of the given leader connection. pub async fn unfollow( &self, room_id: RoomId, diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 178cb712ed5df6f0bcaaed45e5fd4d43996d88b1..7434e2d20d006242ef572a5605c597fa13d71ade 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Clears all room participants in rooms attached to a stale server. pub async fn clear_stale_room_participants( &self, room_id: RoomId, @@ -78,6 +79,7 @@ impl Database { .await } + /// Returns the incoming calls for user with the given ID. pub async fn incoming_call_for_user( &self, user_id: UserId, @@ -102,6 +104,7 @@ impl Database { .await } + /// Creates a new room. pub async fn create_room( &self, user_id: UserId, @@ -394,6 +397,7 @@ impl Database { Ok(participant_index) } + /// Returns the channel ID for the given room, if it has one. pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result> { self.transaction(|tx| async move { let room: Option = room::Entity::find() @@ -944,6 +948,7 @@ impl Database { .await } + /// Updates the location of a participant in the given room. pub async fn update_room_participant_location( &self, room_id: RoomId, @@ -1004,6 +1009,7 @@ impl Database { .await } + /// Sets the role of a participant in the given room. pub async fn set_room_participant_role( &self, admin_id: UserId, diff --git a/crates/collab/src/db/queries/servers.rs b/crates/collab/src/db/queries/servers.rs index e5ceee88873e0e89ecf8a29beef587b00c9baaf9..c79b00eee88654fbbe16ae6c85db159aee4b1a0e 100644 --- a/crates/collab/src/db/queries/servers.rs +++ b/crates/collab/src/db/queries/servers.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Creates a new server in the given environment. pub async fn create_server(&self, environment: &str) -> Result { self.transaction(|tx| async move { let server = server::ActiveModel { @@ -14,6 +15,10 @@ impl Database { .await } + /// Returns the IDs of resources associated with stale servers. + /// + /// A server is stale if it is in the specified `environment` and does not + /// match the provided `new_server_id`. pub async fn stale_server_resource_ids( &self, environment: &str, @@ -61,6 +66,7 @@ impl Database { .await } + /// Deletes any stale servers in the environment that don't match the `new_server_id`. pub async fn delete_stale_servers( &self, environment: &str, diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index 27e64e25981ecdbd31e6aa337875e1ff81852b9c..954ec5f0d80c5b58149cd935abca648fa40a82ef 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -1,6 +1,7 @@ use super::*; impl Database { + /// Creates a new user. pub async fn create_user( &self, email_address: &str, @@ -35,11 +36,13 @@ impl Database { .await } + /// Returns a user by ID. There are no access checks here, so this should only be used internally. pub async fn get_user_by_id(&self, id: UserId) -> Result> { self.transaction(|tx| async move { Ok(user::Entity::find_by_id(id).one(&*tx).await?) }) .await } + /// Returns all users by ID. There are no access checks here, so this should only be used internally. pub async fn get_users_by_ids(&self, ids: Vec) -> Result> { self.transaction(|tx| async { let tx = tx; @@ -51,6 +54,7 @@ impl Database { .await } + /// Returns a user by GitHub login. There are no access checks here, so this should only be used internally. pub async fn get_user_by_github_login(&self, github_login: &str) -> Result> { self.transaction(|tx| async move { Ok(user::Entity::find() @@ -111,6 +115,8 @@ impl Database { .await } + /// get_all_users returns the next page of users. To get more call again with + /// the same limit and the page incremented by 1. pub async fn get_all_users(&self, page: u32, limit: u32) -> Result> { self.transaction(|tx| async move { Ok(user::Entity::find() @@ -123,6 +129,7 @@ impl Database { .await } + /// Returns the metrics id for the user. pub async fn get_user_metrics_id(&self, id: UserId) -> Result { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] enum QueryAs { @@ -142,6 +149,7 @@ impl Database { .await } + /// Set "connected_once" on the user for analytics. pub async fn set_user_connected_once(&self, id: UserId, connected_once: bool) -> Result<()> { self.transaction(|tx| async move { user::Entity::update_many() @@ -157,6 +165,7 @@ impl Database { .await } + /// hard delete the user. pub async fn destroy_user(&self, id: UserId) -> Result<()> { self.transaction(|tx| async move { access_token::Entity::delete_many() @@ -169,6 +178,7 @@ impl Database { .await } + /// Find users where github_login ILIKE name_query. pub async fn fuzzy_search_users(&self, name_query: &str, limit: u32) -> Result> { self.transaction(|tx| async { let tx = tx; @@ -193,6 +203,8 @@ impl Database { .await } + /// fuzzy_like_string creates a string for matching in-order using fuzzy_search_users. + /// e.g. "cir" would become "%c%i%r%" pub fn fuzzy_like_string(string: &str) -> String { let mut result = String::with_capacity(string.len() * 2 + 1); for c in string.chars() { @@ -205,6 +217,7 @@ impl Database { result } + /// Creates a new feature flag. pub async fn create_user_flag(&self, flag: &str) -> Result { self.transaction(|tx| async move { let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { @@ -220,6 +233,7 @@ impl Database { .await } + /// Add the given user to the feature flag pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { self.transaction(|tx| async move { user_feature::Entity::insert(user_feature::ActiveModel { @@ -234,6 +248,7 @@ impl Database { .await } + /// Return the active flags for the user. pub async fn get_user_flags(&self, user: UserId) -> Result> { self.transaction(|tx| async move { #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 739693527f00a594f3376a6093dc8c0b1d270a8f..53866b5c54f96a2e3b42c06515acba4a341bead3 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -2,6 +2,7 @@ use crate::db::UserId; use sea_orm::entity::prelude::*; use serde::Serialize; +/// A user model. #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)] #[sea_orm(table_name = "users")] pub struct Model { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 5d7f68caac9013bf491d1cd37929b1268d075aea..9406b4938a8f2795a89cd8f10238964710eb61f3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -932,11 +932,13 @@ async fn connection_lost( Ok(()) } +/// Acknowledges a ping from a client, used to keep the connection alive. async fn ping(_: proto::Ping, response: Response, _session: Session) -> Result<()> { response.send(proto::Ack {})?; Ok(()) } +/// Create a new room for calling (outside of channels) async fn create_room( _request: proto::CreateRoom, response: Response, @@ -984,6 +986,7 @@ async fn create_room( Ok(()) } +/// Join a room from an invitation. Equivalent to joining a channel if there is one. async fn join_room( request: proto::JoinRoom, response: Response, @@ -1058,6 +1061,7 @@ async fn join_room( Ok(()) } +/// Rejoin room is used to reconnect to a room after connection errors. async fn rejoin_room( request: proto::RejoinRoom, response: Response, @@ -1249,6 +1253,7 @@ async fn rejoin_room( Ok(()) } +/// leave room disonnects from the room. async fn leave_room( _: proto::LeaveRoom, response: Response, @@ -1259,6 +1264,7 @@ async fn leave_room( Ok(()) } +/// Update the permissions of someone else in the room. async fn set_room_participant_role( request: proto::SetRoomParticipantRole, response: Response, @@ -1303,6 +1309,7 @@ async fn set_room_participant_role( Ok(()) } +/// Call someone else into the current room async fn call( request: proto::Call, response: Response, @@ -1371,6 +1378,7 @@ async fn call( Err(anyhow!("failed to ring user"))? } +/// Cancel an outgoing call. async fn cancel_call( request: proto::CancelCall, response: Response, @@ -1408,6 +1416,7 @@ async fn cancel_call( Ok(()) } +/// Decline an incoming call. async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<()> { let room_id = RoomId::from_proto(message.room_id); { @@ -1439,6 +1448,7 @@ async fn decline_call(message: proto::DeclineCall, session: Session) -> Result<( Ok(()) } +/// Update other participants in the room with your current location. async fn update_participant_location( request: proto::UpdateParticipantLocation, response: Response, @@ -1459,6 +1469,7 @@ async fn update_participant_location( Ok(()) } +/// Share a project into the room. async fn share_project( request: proto::ShareProject, response: Response, @@ -1481,6 +1492,7 @@ async fn share_project( Ok(()) } +/// Unshare a project from the room. async fn unshare_project(message: proto::UnshareProject, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(message.project_id); @@ -1500,6 +1512,7 @@ async fn unshare_project(message: proto::UnshareProject, session: Session) -> Re Ok(()) } +/// Join someone elses shared project. async fn join_project( request: proto::JoinProject, response: Response, @@ -1625,6 +1638,7 @@ async fn join_project( Ok(()) } +/// Leave someone elses shared project. async fn leave_project(request: proto::LeaveProject, session: Session) -> Result<()> { let sender_id = session.connection_id; let project_id = ProjectId::from_proto(request.project_id); @@ -1647,6 +1661,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result Ok(()) } +/// Update other participants with changes to the project async fn update_project( request: proto::UpdateProject, response: Response, @@ -1673,6 +1688,7 @@ async fn update_project( Ok(()) } +/// Update other participants with changes to the worktree async fn update_worktree( request: proto::UpdateWorktree, response: Response, @@ -1697,6 +1713,7 @@ async fn update_worktree( Ok(()) } +/// Update other participants with changes to the diagnostics async fn update_diagnostic_summary( message: proto::UpdateDiagnosticSummary, session: Session, @@ -1720,6 +1737,7 @@ async fn update_diagnostic_summary( Ok(()) } +/// Update other participants with changes to the worktree settings async fn update_worktree_settings( message: proto::UpdateWorktreeSettings, session: Session, @@ -1743,6 +1761,7 @@ async fn update_worktree_settings( Ok(()) } +/// Notify other participants that a language server has started. async fn start_language_server( request: proto::StartLanguageServer, session: Session, @@ -1765,6 +1784,7 @@ async fn start_language_server( Ok(()) } +/// Notify other participants that a language server has changed. async fn update_language_server( request: proto::UpdateLanguageServer, session: Session, @@ -1787,6 +1807,8 @@ async fn update_language_server( Ok(()) } +/// forward a project request to the host. These requests should be read only +/// as guests are allowed to send them. async fn forward_read_only_project_request( request: T, response: Response, @@ -1809,6 +1831,8 @@ where Ok(()) } +/// forward a project request to the host. These requests are disallowed +/// for guests. async fn forward_mutating_project_request( request: T, response: Response, @@ -1831,6 +1855,7 @@ where Ok(()) } +/// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, session: Session, @@ -1850,6 +1875,8 @@ async fn create_buffer_for_peer( Ok(()) } +/// Notify other participants that a buffer has been updated. This is +/// allowed for guests as long as the update is limited to selections. async fn update_buffer( request: proto::UpdateBuffer, response: Response, @@ -1909,6 +1936,7 @@ async fn update_buffer( Ok(()) } +/// Notify other participants that a project has been updated. async fn broadcast_project_message_from_host>( request: T, session: Session, @@ -1932,6 +1960,7 @@ async fn broadcast_project_message_from_host, @@ -1969,6 +1998,7 @@ async fn follow( Ok(()) } +/// Stop following another user in a call. async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let project_id = request.project_id.map(ProjectId::from_proto); @@ -2000,6 +2030,7 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> { Ok(()) } +/// Notify everyone following you of your current location. async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> { let room_id = RoomId::from_proto(request.room_id); let database = session.db.lock().await; @@ -2036,6 +2067,7 @@ async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Ok(()) } +/// Get public data about users. async fn get_users( request: proto::GetUsers, response: Response, @@ -2062,6 +2094,7 @@ async fn get_users( Ok(()) } +/// Search for users (to invite) buy Github login async fn fuzzy_search_users( request: proto::FuzzySearchUsers, response: Response, @@ -2092,6 +2125,7 @@ async fn fuzzy_search_users( Ok(()) } +/// Send a contact request to another user. async fn request_contact( request: proto::RequestContact, response: Response, @@ -2138,6 +2172,7 @@ async fn request_contact( Ok(()) } +/// Accept or decline a contact request async fn respond_to_contact_request( request: proto::RespondToContactRequest, response: Response, @@ -2195,6 +2230,7 @@ async fn respond_to_contact_request( Ok(()) } +/// Remove a contact. async fn remove_contact( request: proto::RemoveContact, response: Response, @@ -2245,6 +2281,7 @@ async fn remove_contact( Ok(()) } +/// Create a new channel. async fn create_channel( request: proto::CreateChannel, response: Response, @@ -2279,6 +2316,7 @@ async fn create_channel( Ok(()) } +/// Delete a channel async fn delete_channel( request: proto::DeleteChannel, response: Response, @@ -2308,6 +2346,7 @@ async fn delete_channel( Ok(()) } +/// Invite someone to join a channel. async fn invite_channel_member( request: proto::InviteChannelMember, response: Response, @@ -2344,6 +2383,7 @@ async fn invite_channel_member( Ok(()) } +/// remove someone from a channel async fn remove_channel_member( request: proto::RemoveChannelMember, response: Response, @@ -2385,6 +2425,7 @@ async fn remove_channel_member( Ok(()) } +/// Toggle the channel between public and private async fn set_channel_visibility( request: proto::SetChannelVisibility, response: Response, @@ -2423,6 +2464,7 @@ async fn set_channel_visibility( Ok(()) } +/// Alter the role for a user in the channel async fn set_channel_member_role( request: proto::SetChannelMemberRole, response: Response, @@ -2470,6 +2512,7 @@ async fn set_channel_member_role( Ok(()) } +/// Change the name of a channel async fn rename_channel( request: proto::RenameChannel, response: Response, @@ -2503,6 +2546,7 @@ async fn rename_channel( Ok(()) } +/// Move a channel to a new parent. async fn move_channel( request: proto::MoveChannel, response: Response, @@ -2555,6 +2599,7 @@ async fn notify_channel_moved(result: Option, session: Sessio Ok(()) } +/// Get the list of channel members async fn get_channel_members( request: proto::GetChannelMembers, response: Response, @@ -2569,6 +2614,7 @@ async fn get_channel_members( Ok(()) } +/// Accept or decline a channel invitation. async fn respond_to_channel_invite( request: proto::RespondToChannelInvite, response: Response, @@ -2609,6 +2655,7 @@ async fn respond_to_channel_invite( Ok(()) } +/// Join the channels' room async fn join_channel( request: proto::JoinChannel, response: Response, @@ -2713,6 +2760,7 @@ async fn join_channel_internal( Ok(()) } +/// Start editing the channel notes async fn join_channel_buffer( request: proto::JoinChannelBuffer, response: Response, @@ -2744,6 +2792,7 @@ async fn join_channel_buffer( Ok(()) } +/// Edit the channel notes async fn update_channel_buffer( request: proto::UpdateChannelBuffer, session: Session, @@ -2790,6 +2839,7 @@ async fn update_channel_buffer( Ok(()) } +/// Rejoin the channel notes after a connection blip async fn rejoin_channel_buffers( request: proto::RejoinChannelBuffers, response: Response, @@ -2824,6 +2874,7 @@ async fn rejoin_channel_buffers( Ok(()) } +/// Stop editing the channel notes async fn leave_channel_buffer( request: proto::LeaveChannelBuffer, response: Response, @@ -2885,6 +2936,7 @@ fn send_notifications( } } +/// Send a message to the channel async fn send_channel_message( request: proto::SendChannelMessage, response: Response, @@ -2973,6 +3025,7 @@ async fn send_channel_message( Ok(()) } +/// Delete a channel message async fn remove_channel_message( request: proto::RemoveChannelMessage, response: Response, @@ -2992,6 +3045,7 @@ async fn remove_channel_message( Ok(()) } +/// Mark a channel message as read async fn acknowledge_channel_message( request: proto::AckChannelMessage, session: Session, @@ -3011,6 +3065,7 @@ async fn acknowledge_channel_message( Ok(()) } +/// Mark a buffer version as synced async fn acknowledge_buffer_version( request: proto::AckBufferOperation, session: Session, @@ -3029,6 +3084,7 @@ async fn acknowledge_buffer_version( Ok(()) } +/// Start receiving chat updates for a channel async fn join_channel_chat( request: proto::JoinChannelChat, response: Response, @@ -3049,6 +3105,7 @@ async fn join_channel_chat( Ok(()) } +/// Stop receiving chat updates for a channel async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) -> Result<()> { let channel_id = ChannelId::from_proto(request.channel_id); session @@ -3059,6 +3116,7 @@ async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) Ok(()) } +/// Retrive the chat history for a channel async fn get_channel_messages( request: proto::GetChannelMessages, response: Response, @@ -3082,6 +3140,7 @@ async fn get_channel_messages( Ok(()) } +/// Retrieve specific chat messages async fn get_channel_messages_by_id( request: proto::GetChannelMessagesById, response: Response, @@ -3104,6 +3163,7 @@ async fn get_channel_messages_by_id( Ok(()) } +/// Retrieve the current users notifications async fn get_notifications( request: proto::GetNotifications, response: Response, @@ -3127,6 +3187,7 @@ async fn get_notifications( Ok(()) } +/// Mark notifications as read async fn mark_notification_as_read( request: proto::MarkNotificationRead, response: Response, @@ -3148,6 +3209,7 @@ async fn mark_notification_as_read( Ok(()) } +/// Get the current users information async fn get_private_user_info( _request: proto::GetPrivateUserInfo, response: Response, From d7503a7d47093ae973d18d0a1cd7ec85ec116e77 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 17 Jan 2024 13:39:37 -0500 Subject: [PATCH 40/96] Document LSP crate Co-Authored-By: Thorsten Ball --- crates/lsp/src/lsp.rs | 67 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 788c424373deca7c1490dd954fa005e0943d8a99..0c1574f5aaff21253c1d1eda695939c801283f2e 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -39,6 +39,7 @@ type NotificationHandler = Box, &str, AsyncAppCon type ResponseHandler = Box)>; type IoHandler = Box; +/// Kind of language server stdio given to an IO handler. #[derive(Debug, Clone, Copy)] pub enum IoKind { StdOut, @@ -46,12 +47,15 @@ pub enum IoKind { StdErr, } +/// Represents a launchable language server. This can either be a standalone binary or the path +/// to a runtime with arguments to instruct it to launch the actual language server file. #[derive(Debug, Clone, Deserialize)] pub struct LanguageServerBinary { pub path: PathBuf, pub arguments: Vec, } +/// A running language server process. pub struct LanguageServer { server_id: LanguageServerId, next_id: AtomicUsize, @@ -70,10 +74,12 @@ pub struct LanguageServer { _server: Option>, } +/// Identifies a running language server. #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[repr(transparent)] pub struct LanguageServerId(pub usize); +/// Handle to a language server RPC activity subscription. pub enum Subscription { Notification { method: &'static str, @@ -85,6 +91,9 @@ pub enum Subscription { }, } +/// Language server protocol RPC request message. +/// +/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) #[derive(Serialize, Deserialize)] pub struct Request<'a, T> { jsonrpc: &'static str, @@ -93,6 +102,7 @@ pub struct Request<'a, T> { params: T, } +/// Language server protocol RPC request response message before it is deserialized into a concrete type. #[derive(Serialize, Deserialize)] struct AnyResponse<'a> { jsonrpc: &'a str, @@ -103,6 +113,9 @@ struct AnyResponse<'a> { result: Option<&'a RawValue>, } +/// Language server protocol RPC request response message. +/// +/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#responseMessage) #[derive(Serialize)] struct Response { jsonrpc: &'static str, @@ -111,6 +124,9 @@ struct Response { error: Option, } +/// Language server protocol RPC notification message. +/// +/// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage) #[derive(Serialize, Deserialize)] struct Notification<'a, T> { jsonrpc: &'static str, @@ -119,6 +135,7 @@ struct Notification<'a, T> { params: T, } +/// Language server RPC notification message before it is deserialized into a concrete type. #[derive(Debug, Clone, Deserialize)] struct AnyNotification<'a> { #[serde(default)] @@ -135,6 +152,7 @@ struct Error { } impl LanguageServer { + /// Starts a language server process. pub fn new( stderr_capture: Arc>>, server_id: LanguageServerId, @@ -277,6 +295,7 @@ impl LanguageServer { } } + /// List of code action kinds this language server reports being able to emit. pub fn code_action_kinds(&self) -> Option> { self.code_action_kinds.clone() } @@ -427,9 +446,10 @@ impl LanguageServer { Ok(()) } - /// Initializes a language server. - /// Note that `options` is used directly to construct [`InitializeParams`], - /// which is why it is owned. + /// Initializes a language server by sending the `Initialize` request. + /// Note that `options` is used directly to construct [`InitializeParams`], which is why it is owned. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize) pub async fn initialize(mut self, options: Option) -> Result> { let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] @@ -564,6 +584,7 @@ impl LanguageServer { Ok(Arc::new(self)) } + /// Sends a shutdown request to the language server process and prepares the `LanguageServer` to be dropped. pub fn shutdown(&self) -> Option>> { if let Some(tasks) = self.io_tasks.lock().take() { let response_handlers = self.response_handlers.clone(); @@ -598,6 +619,9 @@ impl LanguageServer { } } + /// Register a handler to handle incoming LSP notifications. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage) #[must_use] pub fn on_notification(&self, f: F) -> Subscription where @@ -607,6 +631,9 @@ impl LanguageServer { self.on_custom_notification(T::METHOD, f) } + /// Register a handler to handle incoming LSP requests. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) #[must_use] pub fn on_request(&self, f: F) -> Subscription where @@ -618,6 +645,7 @@ impl LanguageServer { self.on_custom_request(T::METHOD, f) } + /// Register a handler to inspect all language server process stdio. #[must_use] pub fn on_io(&self, f: F) -> Subscription where @@ -631,20 +659,23 @@ impl LanguageServer { } } + /// Removes a request handler registers via [Self::on_request]. pub fn remove_request_handler(&self) { self.notification_handlers.lock().remove(T::METHOD); } + /// Removes a notification handler registers via [Self::on_notification]. pub fn remove_notification_handler(&self) { self.notification_handlers.lock().remove(T::METHOD); } + /// Checks if a notification handler has been registered via [Self::on_notification]. pub fn has_notification_handler(&self) -> bool { self.notification_handlers.lock().contains_key(T::METHOD) } #[must_use] - pub fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription + fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription where F: 'static + FnMut(Params, AsyncAppContext) + Send, Params: DeserializeOwned, @@ -668,11 +699,7 @@ impl LanguageServer { } #[must_use] - pub fn on_custom_request( - &self, - method: &'static str, - mut f: F, - ) -> Subscription + fn on_custom_request(&self, method: &'static str, mut f: F) -> Subscription where F: 'static + FnMut(Params, AsyncAppContext) -> Fut + Send, Fut: 'static + Future>, @@ -750,22 +777,29 @@ impl LanguageServer { } } + /// Get the name of the running language server. pub fn name(&self) -> &str { &self.name } + /// Get the reported capabilities of the running language server. pub fn capabilities(&self) -> &ServerCapabilities { &self.capabilities } + /// Get the id of the running language server. pub fn server_id(&self) -> LanguageServerId { self.server_id } + /// Get the root path of the project the language server is running against. pub fn root_path(&self) -> &PathBuf { &self.root_path } + /// Sends a RPC request to the language server. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) pub fn request( &self, params: T::Params, @@ -851,6 +885,9 @@ impl LanguageServer { } } + /// Sends a RPC notification to the language server. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage) pub fn notify(&self, params: T::Params) -> Result<()> { Self::notify_internal::(&self.outbound_tx, params) } @@ -879,6 +916,7 @@ impl Drop for LanguageServer { } impl Subscription { + /// Detaching a subscription handle prevents it from unsubscribing on drop. pub fn detach(&mut self) { match self { Subscription::Notification { @@ -925,6 +963,7 @@ impl Drop for Subscription { } } +/// Mock language server for use in tests. #[cfg(any(test, feature = "test-support"))] #[derive(Clone)] pub struct FakeLanguageServer { @@ -946,6 +985,7 @@ impl LanguageServer { } } + /// Construct a fake language server. pub fn fake( name: String, capabilities: ServerCapabilities, @@ -1015,10 +1055,12 @@ impl LanguageServer { #[cfg(any(test, feature = "test-support"))] impl FakeLanguageServer { + /// See [LanguageServer::notify] pub fn notify(&self, params: T::Params) { self.server.notify::(params).ok(); } + /// See [LanguageServer::request] pub async fn request(&self, params: T::Params) -> Result where T: request::Request, @@ -1028,11 +1070,13 @@ impl FakeLanguageServer { self.server.request::(params).await } + /// Attempts [try_receive_notification], unwrapping if it has not received the specified type yet. pub async fn receive_notification(&mut self) -> T::Params { self.server.executor.start_waiting(); self.try_receive_notification::().await.unwrap() } + /// Consumes the notification channel until it finds a notification for the specified type. pub async fn try_receive_notification( &mut self, ) -> Option { @@ -1048,6 +1092,7 @@ impl FakeLanguageServer { } } + /// Registers a handler for a specific kind of request. Removes any existing handler for specified request type. pub fn handle_request( &self, mut handler: F, @@ -1076,6 +1121,7 @@ impl FakeLanguageServer { responded_rx } + /// Registers a handler for a specific kind of notification. Removes any existing handler for specified notification type. pub fn handle_notification( &self, mut handler: F, @@ -1096,6 +1142,7 @@ impl FakeLanguageServer { handled_rx } + /// Removes any existing handler for specified notification type. pub fn remove_request_handler(&mut self) where T: 'static + request::Request, @@ -1103,6 +1150,7 @@ impl FakeLanguageServer { self.server.remove_request_handler::(); } + /// Simulate that the server has started work and notifies about its progress with the specified token. pub async fn start_progress(&self, token: impl Into) { let token = token.into(); self.request::(WorkDoneProgressCreateParams { @@ -1116,6 +1164,7 @@ impl FakeLanguageServer { }); } + /// Simulate that the server has completed work and notifies about that with the specified token. pub fn end_progress(&self, token: impl Into) { self.notify::(ProgressParams { token: NumberOrString::String(token.into()), From 552d2c26f5096e89632e4b6b1b51c23258f49b2d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 17 Jan 2024 11:41:42 -0700 Subject: [PATCH 41/96] Also update chat location when opening a new workspace This happens a lot in guest workflows where they open the call with a link and are jumped straight to a shared workspace. --- crates/collab_ui/src/chat_panel.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index 140d5be4c6548664d1518ff3b6eff5583d3e22ab..44b0669c307bcb491e53e0f07ab48ae461d7f6b3 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -125,6 +125,23 @@ impl ChatPanel { open_context_menu: None, }; + if let Some(channel_id) = ActiveCall::global(cx) + .read(cx) + .room() + .and_then(|room| room.read(cx).channel_id()) + { + this.select_channel(channel_id, None, cx) + .detach_and_log_err(cx); + + if ActiveCall::global(cx) + .read(cx) + .room() + .is_some_and(|room| room.read(cx).contains_guests()) + { + cx.emit(PanelEvent::Activate) + } + } + this.subscriptions.push(cx.subscribe( &ActiveCall::global(cx), move |this: &mut Self, call, event: &room::Event, cx| match event { From 306e4693fad8355c2e5d83f516de70e86449aded Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 Jan 2024 15:11:33 +0200 Subject: [PATCH 42/96] Start adding project search listeners to workspace co-authored-by: Piotr To be able to trigger them from search multibuffer excerpts. --- crates/assistant/src/assistant_panel.rs | 2 +- crates/search/src/buffer_search.rs | 72 +++++++++++++++++----- crates/search/src/project_search.rs | 70 ++++++++++++++++++++- crates/terminal_view/src/terminal_panel.rs | 2 +- 4 files changed, 129 insertions(+), 17 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index df3dc3754f66aff8d83a6fcd3b92edd38c7c4e45..5a8376554e9c66db347dc7012e45208a6ab5f157 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1148,7 +1148,7 @@ impl Render for AssistantPanel { |panel, cx| panel.toolbar.read(cx).item_of_type::(), cx, ); - BufferSearchBar::register_inner(&mut registrar); + BufferSearchBar::register(&mut registrar); registrar.into_div() } else { div() diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e217a7ab73cd2fd1aaa540f1b56cf13b7ec1c84b..4c6d9a5708e3f2965a223a26a979258822ab446e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -496,67 +496,111 @@ impl SearchActionsRegistrar for Workspace { }); } } + impl BufferSearchBar { - pub fn register_inner(registrar: &mut impl SearchActionsRegistrar) { + pub fn register(registrar: &mut impl SearchActionsRegistrar) { registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + if this.supported_options().case { this.toggle_case_sensitive(action, cx); } }); - registrar.register_handler(|this, action: &ToggleWholeWord, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + if this.supported_options().word { this.toggle_whole_word(action, cx); } }); - registrar.register_handler(|this, action: &ToggleReplace, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + if this.supported_options().replacement { this.toggle_replace(action, cx); } }); - registrar.register_handler(|this, _: &ActivateRegexMode, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + if this.supported_options().regex { this.activate_search_mode(SearchMode::Regex, cx); } }); - registrar.register_handler(|this, _: &ActivateTextMode, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + this.activate_search_mode(SearchMode::Text, cx); }); - registrar.register_handler(|this, action: &CycleMode, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + if this.supported_options().regex { // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting // cycling. this.cycle_mode(action, cx) } }); - registrar.register_handler(|this, action: &SelectNextMatch, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + this.select_next_match(action, cx); }); registrar.register_handler(|this, action: &SelectPrevMatch, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + this.select_prev_match(action, cx); }); registrar.register_handler(|this, action: &SelectAllMatches, cx| { + if this.is_dismissed() { + cx.propagate(); + return; + } + this.select_all_matches(action, cx); }); registrar.register_handler(|this, _: &editor::Cancel, cx| { - if this.dismissed { + if this.is_dismissed() { cx.propagate(); - } else { - this.dismiss(&Dismiss, cx); + return; } + + this.dismiss(&Dismiss, cx); }); registrar.register_handler(|this, deploy, cx| { - this.deploy(deploy, cx); + if this.is_dismissed() { + this.deploy(deploy, cx); + return; + } + + cx.propagate(); }) } - fn register(workspace: &mut Workspace) { - Self::register_inner(workspace); - } + pub fn new(cx: &mut ViewContext) -> Self { let query_editor = cx.new_view(|cx| Editor::single_line(cx)); cx.subscribe(&query_editor, Self::on_query_editor_event) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 49eb24ce9ee9e267dc921b6ddb8eb10e92c83c97..30c29a0c089c067d1482074a7940892b3497929d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -63,7 +63,57 @@ pub fn init(cx: &mut AppContext) { workspace .register_action(ProjectSearchView::new_search) .register_action(ProjectSearchView::deploy_search) - .register_action(ProjectSearchBar::search_in_new); + .register_action(ProjectSearchBar::search_in_new) + // TODO kb register these too, consider having the methods for &Workspace for that, as above + // ToggleCaseSensitive + // ToggleWholeWord + // ToggleReplace + // ActivateRegexMode + // SelectPrevMatch + // ActivateTextMode + // ActivateSemanticMode + // CycleMode + // SelectNextMatch (see a proto below) + /* + // Have a generic method similar to the registrar has: + fn register_workspace_action( + &mut workspace, + callback: fn(&mut ProjectSearchBar, &A, &mut ViewContext), + ) { + workspace.register_action(move |workspace, action: &A, cx| { + if workspace.has_active_modal(cx) { + cx.propagate(); + return; + } + if let Some(search_bar) = workspace.active_item(cx).and_then(|item| item.downcast::()) { + search_bar.update(cx, move |this, cx| callback(this, action, cx)); + cx.notify(); + } + }); + } + */ + .register_action(move |workspace, action: &SelectNextMatch, cx| { + dbg!("@@@@@@@@@1"); + if workspace.has_active_modal(cx) { + cx.propagate(); + return; + } + + dbg!("????? 2"); + let pane = workspace.active_pane(); + pane.update(cx, move |this, cx| { + this.toolbar().update(cx, move |this, cx| { + dbg!("@@@@@@@@@ 3"); + if let Some(search_bar) = this.item_of_type::() { + dbg!("$$$$$$$$$ 4"); + search_bar.update(cx, move |search_bar, cx| { + search_bar.select_next_match(action, cx) + }); + cx.notify(); + } + }) + }); + }); }) .detach(); } @@ -1502,6 +1552,22 @@ impl ProjectSearchBar { } } + pub fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { + if let Some(search) = self.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.select_match(Direction::Next, cx); + }) + } + } + + fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { + if let Some(search) = self.active_project_search.as_ref() { + search.update(cx, |this, cx| { + this.select_match(Direction::Prev, cx); + }) + } + } + fn new_placeholder_text(&self, cx: &mut ViewContext) -> Option { let previous_query_keystrokes = cx .bindings_for_action(&PreviousHistoryQuery {}) @@ -1870,6 +1936,8 @@ impl Render for ProjectSearchBar { })) }) }) + .on_action(cx.listener(Self::select_next_match)) + .on_action(cx.listener(Self::select_prev_match)) .child( h_flex() .justify_between() diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 8954e70e8fc18d3d78775ba4eb56b11ec251c0de..7a988851d8a34b8533631e1f74dfcb5d73c45ed1 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -387,7 +387,7 @@ impl Render for TerminalPanel { }, cx, ); - BufferSearchBar::register_inner(&mut registrar); + BufferSearchBar::register(&mut registrar); registrar.into_div().size_full().child(self.pane.clone()) } } From 0be2f7f32891c628fbb02771cfcf7f1453804960 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 Jan 2024 21:09:28 +0200 Subject: [PATCH 43/96] Properly register buffer_search'es actions handlers Now those handlers do not intercept events/actions when the buffer search bar is dismissed. co-authored-by: Piotr --- crates/search/src/buffer_search.rs | 162 +++++++++++++++++----------- crates/search/src/project_search.rs | 4 - 2 files changed, 100 insertions(+), 66 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 4c6d9a5708e3f2965a223a26a979258822ab446e..ee395328c38b459ecff5e19e7d47d97ca719f692 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -429,6 +429,11 @@ pub trait SearchActionsRegistrar { &mut self, callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), ); + + fn register_handler_for_dismissed_bar( + &mut self, + callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ); } type GetSearchBar = @@ -457,16 +462,60 @@ impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> { } impl SearchActionsRegistrar for DivRegistrar<'_, '_, T> { - fn register_handler( + fn register_handler( &mut self, callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), ) { let getter = self.search_getter; self.div = self.div.take().map(|div| { div.on_action(self.cx.listener(move |this, action, cx| { - (getter)(this, cx) + let should_notify = (getter)(this, cx) .clone() - .map(|search_bar| search_bar.update(cx, |this, cx| callback(this, action, cx))); + .map(|search_bar| { + search_bar.update(cx, |search_bar, cx| { + if search_bar.is_dismissed() { + false + } else { + callback(search_bar, action, cx); + true + } + }) + }) + .unwrap_or(false); + if should_notify { + cx.notify(); + } else { + cx.propagate(); + } + })) + }); + } + + fn register_handler_for_dismissed_bar( + &mut self, + callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ) { + let getter = self.search_getter; + self.div = self.div.take().map(|div| { + div.on_action(self.cx.listener(move |this, action, cx| { + let should_notify = (getter)(this, cx) + .clone() + .map(|search_bar| { + search_bar.update(cx, |search_bar, cx| { + if search_bar.is_dismissed() { + callback(search_bar, action, cx); + true + } else { + false + } + }) + }) + .unwrap_or(false); + if should_notify { + cx.notify(); + } else { + cx.propagate(); + } })) }); } @@ -488,71 +537,85 @@ impl SearchActionsRegistrar for Workspace { pane.update(cx, move |this, cx| { this.toolbar().update(cx, move |this, cx| { if let Some(search_bar) = this.item_of_type::() { - search_bar.update(cx, move |this, cx| callback(this, action, cx)); - cx.notify(); + let should_notify = search_bar.update(cx, move |search_bar, cx| { + if search_bar.is_dismissed() { + false + } else { + callback(search_bar, action, cx); + true + } + }); + if should_notify { + cx.notify(); + } else { + cx.propagate(); + } } }) }); }); } -} -impl BufferSearchBar { - pub fn register(registrar: &mut impl SearchActionsRegistrar) { - registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| { - if this.is_dismissed() { + fn register_handler_for_dismissed_bar( + &mut self, + callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ) { + self.register_action(move |workspace, action: &A, cx| { + if workspace.has_active_modal(cx) { cx.propagate(); return; } + let pane = workspace.active_pane(); + pane.update(cx, move |this, cx| { + this.toolbar().update(cx, move |this, cx| { + if let Some(search_bar) = this.item_of_type::() { + let should_notify = search_bar.update(cx, move |search_bar, cx| { + if search_bar.is_dismissed() { + callback(search_bar, action, cx); + true + } else { + false + } + }); + if should_notify { + cx.notify(); + } else { + cx.propagate(); + } + } + }) + }); + }); + } +} + +impl BufferSearchBar { + pub fn register(registrar: &mut impl SearchActionsRegistrar) { + registrar.register_handler(|this, action: &ToggleCaseSensitive, cx| { if this.supported_options().case { this.toggle_case_sensitive(action, cx); } }); registrar.register_handler(|this, action: &ToggleWholeWord, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - if this.supported_options().word { this.toggle_whole_word(action, cx); } }); registrar.register_handler(|this, action: &ToggleReplace, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - if this.supported_options().replacement { this.toggle_replace(action, cx); } }); registrar.register_handler(|this, _: &ActivateRegexMode, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - if this.supported_options().regex { this.activate_search_mode(SearchMode::Regex, cx); } }); registrar.register_handler(|this, _: &ActivateTextMode, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - this.activate_search_mode(SearchMode::Text, cx); }); registrar.register_handler(|this, action: &CycleMode, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - if this.supported_options().regex { // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting // cycling. @@ -560,44 +623,19 @@ impl BufferSearchBar { } }); registrar.register_handler(|this, action: &SelectNextMatch, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - this.select_next_match(action, cx); }); registrar.register_handler(|this, action: &SelectPrevMatch, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - this.select_prev_match(action, cx); }); registrar.register_handler(|this, action: &SelectAllMatches, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - this.select_all_matches(action, cx); }); registrar.register_handler(|this, _: &editor::Cancel, cx| { - if this.is_dismissed() { - cx.propagate(); - return; - } - this.dismiss(&Dismiss, cx); }); - registrar.register_handler(|this, deploy, cx| { - if this.is_dismissed() { - this.deploy(deploy, cx); - return; - } - - cx.propagate(); + registrar.register_handler_for_dismissed_bar(|this, deploy, cx| { + this.deploy(deploy, cx); }) } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 30c29a0c089c067d1482074a7940892b3497929d..a13f70fbe8a394c7b6a092fe4ce4ddf155128312 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -93,19 +93,15 @@ pub fn init(cx: &mut AppContext) { } */ .register_action(move |workspace, action: &SelectNextMatch, cx| { - dbg!("@@@@@@@@@1"); if workspace.has_active_modal(cx) { cx.propagate(); return; } - dbg!("????? 2"); let pane = workspace.active_pane(); pane.update(cx, move |this, cx| { this.toolbar().update(cx, move |this, cx| { - dbg!("@@@@@@@@@ 3"); if let Some(search_bar) = this.item_of_type::() { - dbg!("$$$$$$$$$ 4"); search_bar.update(cx, move |search_bar, cx| { search_bar.select_next_match(action, cx) }); From 65be90937887fcbfb114e7e7a0154579ff371eff Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 Jan 2024 22:07:00 +0200 Subject: [PATCH 44/96] Implement similar workspace registration flow for project search actions --- crates/search/src/buffer_search.rs | 8 +- crates/search/src/project_search.rs | 241 +++++++++++++++++----------- 2 files changed, 153 insertions(+), 96 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index ee395328c38b459ecff5e19e7d47d97ca719f692..e4a68b6105eb0ca6a55e89136822041524391820 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -430,7 +430,7 @@ pub trait SearchActionsRegistrar { callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), ); - fn register_handler_for_dismissed_bar( + fn register_handler_for_dismissed_search( &mut self, callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), ); @@ -491,7 +491,7 @@ impl SearchActionsRegistrar for DivRegistrar<'_, '_, T> { }); } - fn register_handler_for_dismissed_bar( + fn register_handler_for_dismissed_search( &mut self, callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), ) { @@ -556,7 +556,7 @@ impl SearchActionsRegistrar for Workspace { }); } - fn register_handler_for_dismissed_bar( + fn register_handler_for_dismissed_search( &mut self, callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), ) { @@ -634,7 +634,7 @@ impl BufferSearchBar { registrar.register_handler(|this, _: &editor::Cancel, cx| { this.dismiss(&Dismiss, cx); }); - registrar.register_handler_for_dismissed_bar(|this, deploy, cx| { + registrar.register_handler_for_dismissed_search(|this, deploy, cx| { this.deploy(deploy, cx); }) } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index a13f70fbe8a394c7b6a092fe4ce4ddf155128312..098fa184e353ab58b05a6e5709b0eb62f1a5a16f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -12,10 +12,10 @@ use editor::{ }; use editor::{EditorElement, EditorStyle}; use gpui::{ - actions, div, AnyElement, AnyView, AppContext, Context as _, Element, EntityId, EventEmitter, - FocusHandle, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement, IntoElement, - KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, Styled, - Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView, + actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId, + EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, Hsla, InteractiveElement, + IntoElement, KeyContext, Model, ModelContext, ParentElement, PromptLevel, Render, SharedString, + Styled, Subscription, Task, TextStyle, View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, }; use menu::Confirm; @@ -36,6 +36,7 @@ use std::{ time::{Duration, Instant}, }; use theme::ThemeSettings; +use workspace::{DeploySearch, NewSearch}; use ui::{ h_flex, prelude::*, v_flex, Icon, IconButton, IconName, Label, LabelCommon, LabelSize, @@ -60,56 +61,64 @@ struct ActiveSettings(HashMap, ProjectSearchSettings>); pub fn init(cx: &mut AppContext) { cx.set_global(ActiveSettings::default()); cx.observe_new_views(|workspace: &mut Workspace, _cx| { - workspace - .register_action(ProjectSearchView::new_search) - .register_action(ProjectSearchView::deploy_search) - .register_action(ProjectSearchBar::search_in_new) - // TODO kb register these too, consider having the methods for &Workspace for that, as above - // ToggleCaseSensitive - // ToggleWholeWord - // ToggleReplace - // ActivateRegexMode - // SelectPrevMatch - // ActivateTextMode - // ActivateSemanticMode - // CycleMode - // SelectNextMatch (see a proto below) - /* - // Have a generic method similar to the registrar has: - fn register_workspace_action( - &mut workspace, - callback: fn(&mut ProjectSearchBar, &A, &mut ViewContext), - ) { - workspace.register_action(move |workspace, action: &A, cx| { - if workspace.has_active_modal(cx) { - cx.propagate(); - return; - } - if let Some(search_bar) = workspace.active_item(cx).and_then(|item| item.downcast::()) { - search_bar.update(cx, move |this, cx| callback(this, action, cx)); - cx.notify(); - } - }); - } - */ - .register_action(move |workspace, action: &SelectNextMatch, cx| { - if workspace.has_active_modal(cx) { - cx.propagate(); - return; - } + register_workspace_action(workspace, move |search_bar, _: &ToggleFilters, cx| { + search_bar.toggle_filters(cx); + }); + register_workspace_action(workspace, move |search_bar, _: &ToggleCaseSensitive, cx| { + search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + }); + register_workspace_action(workspace, move |search_bar, _: &ToggleWholeWord, cx| { + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + }); + register_workspace_action(workspace, move |search_bar, action: &ToggleReplace, cx| { + search_bar.toggle_replace(action, cx) + }); + register_workspace_action(workspace, move |search_bar, _: &ActivateRegexMode, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx) + }); + register_workspace_action(workspace, move |search_bar, _: &ActivateTextMode, cx| { + search_bar.activate_search_mode(SearchMode::Text, cx) + }); + register_workspace_action( + workspace, + move |search_bar, _: &ActivateSemanticMode, cx| { + search_bar.activate_search_mode(SearchMode::Semantic, cx) + }, + ); + register_workspace_action(workspace, move |search_bar, action: &CycleMode, cx| { + search_bar.cycle_mode(action, cx) + }); + register_workspace_action( + workspace, + move |search_bar, action: &SelectNextMatch, cx| { + search_bar.select_next_match(action, cx) + }, + ); + register_workspace_action( + workspace, + move |search_bar, action: &SelectPrevMatch, cx| { + search_bar.select_prev_match(action, cx) + }, + ); - let pane = workspace.active_pane(); - pane.update(cx, move |this, cx| { - this.toolbar().update(cx, move |this, cx| { - if let Some(search_bar) = this.item_of_type::() { - search_bar.update(cx, move |search_bar, cx| { - search_bar.select_next_match(action, cx) - }); - cx.notify(); - } - }) - }); - }); + register_workspace_action_for_dismissed_search( + workspace, + move |workspace, action: &NewSearch, cx| { + ProjectSearchView::new_search(workspace, action, cx) + }, + ); + register_workspace_action_for_dismissed_search( + workspace, + move |workspace, action: &DeploySearch, cx| { + ProjectSearchView::deploy_search(workspace, action, cx) + }, + ); + register_workspace_action_for_dismissed_search( + workspace, + move |workspace, action: &SearchInNew, cx| { + ProjectSearchView::search_in_new(workspace, action, cx) + }, + ); }) .detach(); } @@ -1006,6 +1015,37 @@ impl ProjectSearchView { Self::existing_or_new_search(workspace, existing, cx) } + fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let new_query = search_view.update(cx, |search_view, cx| { + let new_query = search_view.build_search_query(cx); + if new_query.is_some() { + if let Some(old_query) = search_view.model.read(cx).active_query.clone() { + search_view.query_editor.update(cx, |editor, cx| { + editor.set_text(old_query.as_str(), cx); + }); + search_view.search_options = SearchOptions::from_query(&old_query); + } + } + new_query + }); + if let Some(new_query) = new_query { + let model = cx.new_model(|cx| { + let mut model = ProjectSearch::new(workspace.project().clone(), cx); + model.search(new_query, cx); + model + }); + workspace.add_item( + Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))), + cx, + ); + } + } + } + // Add another search tab to the workspace. fn new_search( workspace: &mut Workspace, @@ -1308,17 +1348,11 @@ impl ProjectSearchView { } } -impl Default for ProjectSearchBar { - fn default() -> Self { - Self::new() - } -} - impl ProjectSearchBar { pub fn new() -> Self { Self { - active_project_search: Default::default(), - subscription: Default::default(), + active_project_search: None, + subscription: None, } } @@ -1349,37 +1383,6 @@ impl ProjectSearchBar { } } - fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let new_query = search_view.update(cx, |search_view, cx| { - let new_query = search_view.build_search_query(cx); - if new_query.is_some() { - if let Some(old_query) = search_view.model.read(cx).active_query.clone() { - search_view.query_editor.update(cx, |editor, cx| { - editor.set_text(old_query.as_str(), cx); - }); - search_view.search_options = SearchOptions::from_query(&old_query); - } - } - new_query - }); - if let Some(new_query) = new_query { - let model = cx.new_model(|cx| { - let mut model = ProjectSearch::new(workspace.project().clone(), cx); - model.search(new_query, cx); - model - }); - workspace.add_item( - Box::new(cx.new_view(|cx| ProjectSearchView::new(model, cx, None))), - cx, - ); - } - } - } - fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { self.cycle_field(Direction::Next, cx); } @@ -2027,6 +2030,60 @@ impl ToolbarItemView for ProjectSearchBar { } } +fn register_workspace_action( + workspace: &mut Workspace, + callback: fn(&mut ProjectSearchBar, &A, &mut ViewContext), +) { + workspace.register_action(move |workspace, action: &A, cx| { + if workspace.has_active_modal(cx) { + cx.propagate(); + return; + } + + workspace.active_pane().update(cx, |pane, cx| { + pane.toolbar().update(cx, move |workspace, cx| { + if let Some(search_bar) = workspace.item_of_type::() { + search_bar.update(cx, move |search_bar, cx| { + if search_bar.active_project_search.is_some() { + callback(search_bar, action, cx); + cx.notify(); + } else { + cx.propagate(); + } + }); + } + }); + }) + }); +} + +fn register_workspace_action_for_dismissed_search( + workspace: &mut Workspace, + callback: fn(&mut Workspace, &A, &mut ViewContext), +) { + workspace.register_action(move |workspace, action: &A, cx| { + if workspace.has_active_modal(cx) { + cx.propagate(); + return; + } + + let should_notify = workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .map(|search_bar| search_bar.read(cx).active_project_search.is_none()) + .unwrap_or(false); + if should_notify { + callback(workspace, action, cx); + cx.notify(); + } else { + cx.propagate(); + } + }); +} + #[cfg(test)] pub mod tests { use super::*; From 1e6757755ef5b34089d90aa8733a248fa8afe74f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 17 Jan 2024 22:18:54 +0200 Subject: [PATCH 45/96] Ignore buffer search events if it's not for the current buffer --- crates/search/src/buffer_search.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e4a68b6105eb0ca6a55e89136822041524391820..fbc7101355e08a0701f71559e98b4711a076d38d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -473,7 +473,9 @@ impl SearchActionsRegistrar for DivRegistrar<'_, '_, T> { .clone() .map(|search_bar| { search_bar.update(cx, |search_bar, cx| { - if search_bar.is_dismissed() { + if search_bar.is_dismissed() + || search_bar.active_searchable_item.is_none() + { false } else { callback(search_bar, action, cx); @@ -538,7 +540,9 @@ impl SearchActionsRegistrar for Workspace { this.toolbar().update(cx, move |this, cx| { if let Some(search_bar) = this.item_of_type::() { let should_notify = search_bar.update(cx, move |search_bar, cx| { - if search_bar.is_dismissed() { + if search_bar.is_dismissed() + || search_bar.active_searchable_item.is_none() + { false } else { callback(search_bar, action, cx); From b2afa7332149a5c8d8999e75df2988b26c7b0b9b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 15:45:17 -0500 Subject: [PATCH 46/96] Decrease the size of timestamps in the assistant conversation editor (#4101) This PR decreases the size of the timestamps in the assistant's conversation editor. Ideally we'd want to align the baseline of the timestamp text with the text in the sender button. I spent a while trying to do this, but it seems like it may be pretty tricky. Release Notes: - Decreased the size of timestamps in the assistant panel conversation editor. --- crates/assistant/src/assistant_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 5a8376554e9c66db347dc7012e45208a6ab5f157..241b9af923e44da0139bf07d3a57631c6c7e353b 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2311,8 +2311,7 @@ impl ConversationEditor { } }); - div() - .h_flex() + h_flex() .id(("message_header", message_id.0)) .h_11() .relative() @@ -2328,6 +2327,7 @@ impl ConversationEditor { .add_suffix(true) .to_string(), ) + .size(LabelSize::XSmall) .color(Color::Muted), ) .children( From 9367f719f267f7981684cdbdb32b114ca51d9e4d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Jan 2024 12:45:18 -0800 Subject: [PATCH 47/96] Rework db-seeding, so that it doesn't depend on a github auth token Instead, admins are specified using a JSON file, 'admins.json'. This file is gitignored. If it is not present, there is a default list of admins in 'admins.default.json'. --- .gitignore | 1 + crates/collab/.admins.default.json | 1 + crates/collab/src/bin/seed.rs | 121 ++++++++++++-------------- crates/collab/src/db/queries/users.rs | 6 +- script/seed-db | 4 - script/zed-local | 39 +++++---- 6 files changed, 84 insertions(+), 88 deletions(-) create mode 100644 crates/collab/.admins.default.json diff --git a/.gitignore b/.gitignore index 2d8807a4b0559751ff341eacf7dfaf51c84c405c..6923b060f6ac1f1fc50b423100edbfd5006d5909 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /styles/src/types/zed.ts /crates/theme/schemas/theme.json /crates/collab/static/styles.css +/crates/collab/.admins.json /vendor/bin /assets/themes/*.json /assets/*licenses.md diff --git a/crates/collab/.admins.default.json b/crates/collab/.admins.default.json new file mode 100644 index 0000000000000000000000000000000000000000..6ee4d8726a303be4457078be9353402cbd712f20 --- /dev/null +++ b/crates/collab/.admins.default.json @@ -0,0 +1 @@ +["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler"] diff --git a/crates/collab/src/bin/seed.rs b/crates/collab/src/bin/seed.rs index 88fe0a647b8924b2df1312aa8a9a3bd68b5d99f1..ed24ccef75dce446eb54a431d1371139d67b7140 100644 --- a/crates/collab/src/bin/seed.rs +++ b/crates/collab/src/bin/seed.rs @@ -1,7 +1,11 @@ -use collab::{db, executor::Executor}; +use collab::{ + db::{self, NewUserParams}, + env::load_dotenv, + executor::Executor, +}; use db::{ConnectOptions, Database}; use serde::{de::DeserializeOwned, Deserialize}; -use std::fmt::Write; +use std::{fmt::Write, fs}; #[derive(Debug, Deserialize)] struct GitHubUser { @@ -12,90 +16,75 @@ struct GitHubUser { #[tokio::main] async fn main() { + load_dotenv().expect("failed to load .env.toml file"); + + let mut admin_logins = + load_admins("./.admins.default.json").expect("failed to load default admins file"); + if let Ok(other_admins) = load_admins("./.admins.json") { + admin_logins.extend(other_admins); + } + let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var"); let db = Database::new(ConnectOptions::new(database_url), Executor::Production) .await .expect("failed to connect to postgres database"); - let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var"); let client = reqwest::Client::new(); - let mut current_user = - fetch_github::(&client, &github_token, "https://api.github.com/user").await; - current_user - .email - .get_or_insert_with(|| "placeholder@example.com".to_string()); - let staff_users = fetch_github::>( - &client, - &github_token, - "https://api.github.com/orgs/zed-industries/teams/staff/members", - ) - .await; - - let mut zed_users = Vec::new(); - zed_users.push((current_user, true)); - zed_users.extend(staff_users.into_iter().map(|user| (user, true))); + // Create admin users for all of the users in `.admins.toml` or `.admins.default.toml`. + for admin_login in admin_logins { + let user = fetch_github::( + &client, + &format!("https://api.github.com/users/{admin_login}"), + ) + .await; + db.create_user( + &user.email.unwrap_or(format!("{admin_login}@example.com")), + true, + NewUserParams { + github_login: user.login, + github_user_id: user.id, + }, + ) + .await + .expect("failed to create admin user"); + } - let user_count = db + // Fetch 100 other random users from GitHub and insert them into the database. + let mut user_count = db .get_all_users(0, 200) .await .expect("failed to load users from db") .len(); - if user_count < 100 { - let mut last_user_id = None; - for _ in 0..10 { - let mut uri = "https://api.github.com/users?per_page=100".to_string(); - if let Some(last_user_id) = last_user_id { - write!(&mut uri, "&since={}", last_user_id).unwrap(); - } - let users = fetch_github::>(&client, &github_token, &uri).await; - if let Some(last_user) = users.last() { - last_user_id = Some(last_user.id); - zed_users.extend(users.into_iter().map(|user| (user, false))); - } else { - break; - } + let mut last_user_id = None; + while user_count < 100 { + let mut uri = "https://api.github.com/users?per_page=100".to_string(); + if let Some(last_user_id) = last_user_id { + write!(&mut uri, "&since={}", last_user_id).unwrap(); } - } + let users = fetch_github::>(&client, &uri).await; - for (github_user, admin) in zed_users { - if db - .get_user_by_github_login(&github_user.login) + for github_user in users { + last_user_id = Some(github_user.id); + user_count += 1; + db.get_or_create_user_by_github_account( + &github_user.login, + Some(github_user.id), + github_user.email.as_deref(), + ) .await - .expect("failed to fetch user") - .is_none() - { - if admin { - db.create_user( - &format!("{}@zed.dev", github_user.login), - admin, - db::NewUserParams { - github_login: github_user.login, - github_user_id: github_user.id, - }, - ) - .await - .expect("failed to insert user"); - } else { - db.get_or_create_user_by_github_account( - &github_user.login, - Some(github_user.id), - github_user.email.as_deref(), - ) - .await - .expect("failed to insert user"); - } + .expect("failed to insert user"); } } } -async fn fetch_github( - client: &reqwest::Client, - access_token: &str, - url: &str, -) -> T { +fn load_admins(path: &str) -> anyhow::Result> { + let file_content = fs::read_to_string(path)?; + Ok(serde_json::from_str(&file_content)?) +} + +async fn fetch_github(client: &reqwest::Client, url: &str) -> T { let response = client .get(url) - .bearer_auth(&access_token) .header("user-agent", "zed") .send() .await diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index 954ec5f0d80c5b58149cd935abca648fa40a82ef..8f975b5cbe5d8b4239e34e8770b57b979d6ac378 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -20,7 +20,11 @@ impl Database { }) .on_conflict( OnConflict::column(user::Column::GithubLogin) - .update_column(user::Column::GithubLogin) + .update_columns([ + user::Column::Admin, + user::Column::EmailAddress, + user::Column::GithubUserId, + ]) .to_owned(), ) .exec_with_returning(&*tx) diff --git a/script/seed-db b/script/seed-db index 6bb0f969330fec3936d2008c6013e9b37b8437cd..277ea89ba3b8e8dc056f6fab052531cec86cf102 100755 --- a/script/seed-db +++ b/script/seed-db @@ -2,8 +2,4 @@ set -e cd crates/collab - -# Export contents of .env.toml -eval "$(cargo run --quiet --bin dotenv)" - cargo run --quiet --package=collab --features seed-support --bin seed -- $@ diff --git a/script/zed-local b/script/zed-local index 090fbd58760cd04779340be19d06a199d2a0a01d..4ae4013a4c3fe7b4eaee1b24b7ea8c5bc70a2259 100755 --- a/script/zed-local +++ b/script/zed-local @@ -4,6 +4,11 @@ const HELP = ` USAGE zed-local [options] [zed args] +SUMMARY + Runs 1-4 instances of Zed using a locally-running collaboration server. + Each instance of Zed will be signed in as a different user specified in + either \`.admins.json\` or \`.admins.default.json\`. + OPTIONS --help Print this help message --release Build Zed in release mode @@ -12,6 +17,16 @@ OPTIONS `.trim(); const { spawn, execFileSync } = require("child_process"); +const assert = require("assert"); + +const defaultUsers = require("../crates/collab/.admins.default.json"); +let users = defaultUsers; +try { + const customUsers = require("../crates/collab/.admins.json"); + assert(customUsers.length > 0); + assert(customUsers.every((user) => typeof user === "string")); + users.splice(0, 0, ...customUsers); +} catch (_) {} const RESOLUTION_REGEX = /(\d+) x (\d+)/; const DIGIT_FLAG_REGEX = /^--?(\d+)$/; @@ -71,10 +86,6 @@ if (instanceCount > 1) { } } -let users = ["nathansobo", "as-cii", "maxbrunsfeld", "iamnbutler"]; - -const RUST_LOG = process.env.RUST_LOG || "info"; - // If a user is specified, make sure it's first in the list const user = process.env.ZED_IMPERSONATE; if (user) { @@ -88,18 +99,12 @@ const positions = [ `${instanceWidth},${instanceHeight}`, ]; -const buildArgs = (() => { - const buildArgs = ["build"]; - if (isReleaseMode) { - buildArgs.push("--release"); - } - - return buildArgs; -})(); -const zedBinary = (() => { - const target = isReleaseMode ? "release" : "debug"; - return `target/${target}/Zed`; -})(); +let buildArgs = ["build"]; +let zedBinary = "target/debug/Zed"; +if (isReleaseMode) { + buildArgs.push("--release"); + zedBinary = "target/release/Zed"; +} execFileSync("cargo", buildArgs, { stdio: "inherit" }); setTimeout(() => { @@ -115,7 +120,7 @@ setTimeout(() => { ZED_ADMIN_API_TOKEN: "secret", ZED_WINDOW_SIZE: `${instanceWidth},${instanceHeight}`, PATH: process.env.PATH, - RUST_LOG, + RUST_LOG: process.env.RUST_LOG || "info", }, }); } From ad2b4f288e493d0f3564cd220c7055776c88298e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Jan 2024 13:24:55 -0800 Subject: [PATCH 48/96] Update procfile and local development docs, zed.dev is no longer needed --- Procfile | 2 - docs/src/SUMMARY.md | 3 + docs/src/developing_zed__building_zed.md | 136 +++++------------- .../developing_zed__local_collaboration.md | 52 +++++-- 4 files changed, 79 insertions(+), 114 deletions(-) diff --git a/Procfile b/Procfile index 3f42c3a9677477bd7dcd04ca61a749206559be40..7bd9114dad4ec3e89c4699d0924bbf1ef1243867 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,2 @@ -web: cd ../zed.dev && PORT=3000 npm run dev collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve livekit: livekit-server --dev -postgrest: postgrest crates/collab/admin_api.conf diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index ad1cd6332c4a5200a66ff5767db3673abc88f921..e300e9906956c9eb44fe730de93f4de4a8eea90a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -4,15 +4,18 @@ [Feedback](./feedback.md) # Configuring Zed + - [Settings](./configuring_zed.md) - [Vim Mode](./configuring_zed__configuring_vim.md) # Using Zed + - [Workflows]() - [Collaboration]() - [Using AI]() # Contributing to Zed + - [How to Contribute]() - [Building from Source](./developing_zed__building_zed.md) - [Local Collaboration](./developing_zed__local_collaboration.md) diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index a5270e2b2abc8f89e88e23d016e2e3a21a065efd..7535ceb4d0193e01b46e1cf1f9e2c818c086f138 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -1,133 +1,73 @@ # Building Zed -🚧 TODO: +## Dependencies -- [ ] Remove ZI-specific things -- [ ] Rework any steps that currently require a ZI-specific account +- Install [Rust](https://www.rust-lang.org/tools/install) +- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store -How to build Zed from source for the first time. +- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) -### Prerequisites + ```bash + xcode-select --install + ``` -🚧 TODO 🚧 Update for open source +- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: -- Be added to the GitHub organization -- Be added to the Vercel team -- Create a [Personal Access Token](https://github.com/settings/personal-access-tokens/new) on Github - - 🚧 TODO 🚧 What permissions are required? - - 🚧 TODO 🚧 What changes when repo isn't private? - - Go to https://github.com/settings/tokens and Generate new token - - GitHub currently provides two kinds of tokens: - - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected - Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories - - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos - - Keep the token in the browser tab/editor for the next two steps + ``` + sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer. + ``` -### Dependencies +* Install the Rust wasm toolchain: -- Install [Rust](https://www.rust-lang.org/tools/install) + ```bash + rustup target add wasm32-wasi + ``` -- Install the [GitHub CLI](https://cli.github.com/), [Livekit](https://formulae.brew.sh/formula/livekit) & [Foreman](https://formulae.brew.sh/formula/foreman) +## Backend Dependencies -```bash -brew install gh -brew install livekit -brew install foreman -``` +If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: -- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store +- Install [Postgres](https://postgresapp.com) +- Install [Livekit](https://formulae.brew.sh/formula/livekit) and [Foreman](https://formulae.brew.sh/formula/foreman) -- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) + ```bash + brew install livekit foreman + ``` -```bash -xcode-select --install -``` - -- If `xcode-select --print-path prints /Library/Developer/CommandLineTools…` run `sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer.` +## Building Zed from Source -* Install [Postgres](https://postgresapp.com) +Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). -* Install the wasm toolchain +For a debug build: -```bash -rustup target add wasm32-wasi ``` - -### Building Zed from Source - -1. Clone the `zed` repo - -```bash -gh repo clone zed-industries/zed +cargo run ``` -1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` -1. (🚧 TODO 🚧 - Will this be relevant for open source?) Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: +For a release build: -```bash -cd .. -git clone https://github.com/zed-industries/zed.dev -cd zed.dev && npm install -pnpm install -g vercel +``` +cargo run --release ``` -1. (🚧 TODO 🚧 - Will this be relevant for open source?) Link your zed.dev project to Vercel - -- `vercel link` -- Select the `zed-industries` team. If you don't have this get someone on the team to add you to it. -- Select the `zed.dev` project - -1. (🚧 TODO 🚧 - Will this be relevant for open source?) Run `vercel pull` to pull down the environment variables and project info from Vercel -1. Open Postgres.app -1. From `./path/to/zed/` run `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` - -- You don't need to include the GITHUB_TOKEN if you exported it above. -- Consider removing the token (if it's fine for you to recreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). +And to run the tests: -1. To run the Zed app: - - If you are working on zed: - - `cargo run` - - If you are just using the latest version, but not working on zed: - - `cargo run --release` - - If you need to run the collaboration server locally: - - `script/zed-local` +``` +cargo test --workspace +``` ## Troubleshooting -**`error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`** - -- Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer` - -**`xcrun: error: unable to find utility "metal", not a developer tool or in PATH`** +### Error compiling metal shaders -### `script/bootstrap` - -```bash -Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)! -Please create a new installation in /opt/homebrew using one of the -"Alternative Installs" from: -https://docs.brew.sh/Installation ``` +error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)`** -- In that case try `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` - -- If Homebrew is not in your PATH: - - Replace `{username}` with your home folder name (usually your login name) - - `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile` - - `eval "$(/opt/homebrew/bin/brew shellenv)"` - +xcrun: error: unable to find utility "metal", not a developer tool or in PATH ``` -seeding database... -thread 'main' panicked at 'failed to deserialize github user from 'https://api.github.com/orgs/zed-industries/teams/staff/members': reqwest::Error { kind: Decode, source: Error("invalid type: map, expected a sequence", line: 1, column: 0) }', crates/collab/src/bin/seed.rs:111:10 -``` - -Wrong permissions for `GITHUB_TOKEN` token used, the token needs to be able to read from private repos. -For Classic GitHub Tokens, that required OAuth scope `repo` (seacrh the scope name above for more details) - -Same command -`sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` +Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer` -### If you experience errors that mention some dependency is using unstable features +### Cargo errors claiming that a dependency is using unstable features Try `cargo clean` and `cargo build`, diff --git a/docs/src/developing_zed__local_collaboration.md b/docs/src/developing_zed__local_collaboration.md index 7bbbda36457174816ccb0da6d98dac8543b5f065..0fc08ef767df89ab9c059f30dbbc4cb95c227129 100644 --- a/docs/src/developing_zed__local_collaboration.md +++ b/docs/src/developing_zed__local_collaboration.md @@ -1,22 +1,46 @@ # Local Collaboration -## Setting up the local collaboration server +First, make sure you've installed Zed's [backend dependencies](/developing_zed__building_zed.html#backend-dependencies). -### Setting up for the first time? +## Database setup -1. Make sure you have livekit installed (`brew install livekit`) -1. Install [Postgres](https://postgresapp.com) and run it. -1. Then, from the root of the repo, run `script/bootstrap`. +Before you can run the `collab` server locally, you'll need to set up a `zed` Postgres database. -### Have a db that is out of date? / Need to migrate? +``` +script/bootstrap +``` -1. Make sure you have livekit installed (`brew install livekit`) -1. Try `cd crates/collab && cargo run -- migrate` from the root of the repo. -1. Run `script/seed-db` +This script will set up the `zed` Postgres database, and populate it with some users. It requires internet access, because it fetches some users from the GitHub API. -## Testing collab locally +The script will create several *admin* users, who you'll sign in as by default when developing locally. The GitHub logins for these default admin users are specified in this file: -1. Run `foreman start` from the root of the repo. -1. In another terminal run `script/zed-local`. -1. Two copies of Zed will open. Add yourself as a contact in the one that is not you. -1. Start a collaboration session as normal with any open project. +``` +cat crates/collab/.admins.default.json +``` + +To use a different set of admin users, you can create a file called `.admins.json` in the same directory: + +``` +cat > crates/collab/.admins.json < Date: Wed, 17 Jan 2024 13:00:29 -0800 Subject: [PATCH 49/96] Drop scroll events if there's been a reset co-authored-by: Nathan co-authored-by: Conrad --- crates/gpui/src/app/test_context.rs | 3 +- crates/gpui/src/elements/list.rs | 201 ++++++++++++++-------------- crates/gpui/src/keymap/matcher.rs | 1 - crates/gpui/src/window.rs | 1 - 4 files changed, 102 insertions(+), 104 deletions(-) diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 41cb722081b60b9b404244ed0e12e126dd63b279..d95558f058a91bb4a7cd9a3ab347ee10584bffba 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -643,7 +643,8 @@ impl<'a> VisualTestContext { /// Simulate an event from the platform, e.g. a SrollWheelEvent /// Make sure you've called [VisualTestContext::draw] first! pub fn simulate_event(&mut self, event: E) { - self.update(|cx| cx.dispatch_event(event.to_platform_input())); + self.test_window(self.window) + .simulate_input(event.to_platform_input()); self.background_executor.run_until_parked(); } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 9d49149e7abe7333f42abe5eb07b32e697098774..c0874a8dd4116275846edc24c1ebcfcfd9d752b6 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -28,9 +28,9 @@ struct StateInner { render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, - pending_scroll_delta: Pixels, alignment: ListAlignment, overdraw: Pixels, + reset: bool, #[allow(clippy::type_complexity)] scroll_handler: Option>, } @@ -93,12 +93,17 @@ impl ListState { alignment: orientation, overdraw, scroll_handler: None, - pending_scroll_delta: px(0.), + reset: false, }))) } + /// Reset this instantiation of the list state. + /// + /// Note that this will cause scroll events to be dropped until the next paint. pub fn reset(&self, element_count: usize) { let state = &mut *self.0.borrow_mut(); + state.reset = true; + state.logical_scroll_top = None; state.items = SumTree::new(); state @@ -154,11 +159,13 @@ impl ListState { scroll_top.item_ix = item_count; scroll_top.offset_in_item = px(0.); } + state.logical_scroll_top = Some(scroll_top); } pub fn scroll_to_reveal_item(&self, ix: usize) { let state = &mut *self.0.borrow_mut(); + let mut scroll_top = state.logical_scroll_top(); let height = state .last_layout_bounds @@ -189,9 +196,9 @@ impl ListState { /// Get the bounds for the given item in window coordinates. pub fn bounds_for_item(&self, ix: usize) -> Option> { let state = &*self.0.borrow(); + let bounds = state.last_layout_bounds.unwrap_or_default(); let scroll_top = state.logical_scroll_top(); - if ix < scroll_top.item_ix { return None; } @@ -232,7 +239,11 @@ impl StateInner { delta: Point, cx: &mut WindowContext, ) { - // self.pending_scroll_delta += delta.y; + // Drop scroll events after a reset, since we can't calculate + // the new logical scroll top without the item heights + if self.reset { + return; + } let scroll_max = (self.items.summary().height - height).max(px(0.)); let new_scroll_top = (self.scroll_top(scroll_top) - delta.y) @@ -329,6 +340,8 @@ impl Element for List { ) { let state = &mut *self.state.0.borrow_mut(); + state.reset = false; + // If the width of the list has changed, invalidate all cached item heights if state.last_layout_bounds.map_or(true, |last_bounds| { last_bounds.size.width != bounds.size.width @@ -352,117 +365,104 @@ impl Element for List { let mut cursor = old_items.cursor::(); - loop { - // Render items after the scroll top, including those in the trailing overdraw - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); - for (ix, item) in cursor.by_ref().enumerate() { - let visible_height = rendered_height - scroll_top.offset_in_item; - if visible_height >= bounds.size.height + state.overdraw { - break; - } - - // Use the previously cached height if available - let mut height = if let ListItem::Rendered { height } = item { - Some(*height) - } else { - None - }; - - // If we're within the visible area or the height wasn't cached, render and measure the item's element - if visible_height < bounds.size.height || height.is_none() { - let mut element = (state.render_item)(scroll_top.item_ix + ix, cx); - let element_size = element.measure(available_item_space, cx); - height = Some(element_size.height); - if visible_height < bounds.size.height { - item_elements.push_back(element); - } - } - - let height = height.unwrap(); - rendered_height += height; - measured_items.push_back(ListItem::Rendered { height }); + // Render items after the scroll top, including those in the trailing overdraw + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + for (ix, item) in cursor.by_ref().enumerate() { + let visible_height = rendered_height - scroll_top.offset_in_item; + if visible_height >= bounds.size.height + state.overdraw { + break; } - // Prepare to start walking upward from the item at the scroll top. - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); - - // If the rendered items do not fill the visible region, then adjust - // the scroll top upward. - if rendered_height - scroll_top.offset_in_item < bounds.size.height { - while rendered_height < bounds.size.height { - cursor.prev(&()); - if cursor.item().is_some() { - let mut element = (state.render_item)(cursor.start().0, cx); - let element_size = element.measure(available_item_space, cx); - - rendered_height += element_size.height; - measured_items.push_front(ListItem::Rendered { - height: element_size.height, - }); - item_elements.push_front(element) - } else { - break; - } + // Use the previously cached height if available + let mut height = if let ListItem::Rendered { height } = item { + Some(*height) + } else { + None + }; + + // If we're within the visible area or the height wasn't cached, render and measure the item's element + if visible_height < bounds.size.height || height.is_none() { + let mut element = (state.render_item)(scroll_top.item_ix + ix, cx); + let element_size = element.measure(available_item_space, cx); + height = Some(element_size.height); + if visible_height < bounds.size.height { + item_elements.push_back(element); } + } - scroll_top = ListOffset { - item_ix: cursor.start().0, - offset_in_item: rendered_height - bounds.size.height, - }; + let height = height.unwrap(); + rendered_height += height; + measured_items.push_back(ListItem::Rendered { height }); + } - match state.alignment { - ListAlignment::Top => { - scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); - state.logical_scroll_top = Some(scroll_top); - } - ListAlignment::Bottom => { - scroll_top = ListOffset { - item_ix: cursor.start().0, - offset_in_item: rendered_height - bounds.size.height, - }; - state.logical_scroll_top = None; - } - }; - } + // Prepare to start walking upward from the item at the scroll top. + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); - // Measure items in the leading overdraw - let mut leading_overdraw = scroll_top.offset_in_item; - while leading_overdraw < state.overdraw { + // If the rendered items do not fill the visible region, then adjust + // the scroll top upward. + if rendered_height - scroll_top.offset_in_item < bounds.size.height { + while rendered_height < bounds.size.height { cursor.prev(&()); - if let Some(item) = cursor.item() { - let height = if let ListItem::Rendered { height } = item { - *height - } else { - let mut element = (state.render_item)(cursor.start().0, cx); - element.measure(available_item_space, cx).height - }; + if cursor.item().is_some() { + let mut element = (state.render_item)(cursor.start().0, cx); + let element_size = element.measure(available_item_space, cx); - leading_overdraw += height; - measured_items.push_front(ListItem::Rendered { height }); + rendered_height += element_size.height; + measured_items.push_front(ListItem::Rendered { + height: element_size.height, + }); + item_elements.push_front(element) } else { break; } } - let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); - let mut cursor = old_items.cursor::(); - let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &()); - new_items.extend(measured_items, &()); - cursor.seek(&Count(measured_range.end), Bias::Right, &()); - new_items.append(cursor.suffix(&()), &()); + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - bounds.size.height, + }; - state.items = new_items; - state.last_layout_bounds = Some(bounds); + match state.alignment { + ListAlignment::Top => { + scroll_top.offset_in_item = scroll_top.offset_in_item.max(px(0.)); + state.logical_scroll_top = Some(scroll_top); + } + ListAlignment::Bottom => { + scroll_top = ListOffset { + item_ix: cursor.start().0, + offset_in_item: rendered_height - bounds.size.height, + }; + state.logical_scroll_top = None; + } + }; + } - // if !state.pending_scroll_delta.is_zero() { - // // Do scroll manipulation + // Measure items in the leading overdraw + let mut leading_overdraw = scroll_top.offset_in_item; + while leading_overdraw < state.overdraw { + cursor.prev(&()); + if let Some(item) = cursor.item() { + let height = if let ListItem::Rendered { height } = item { + *height + } else { + let mut element = (state.render_item)(cursor.start().0, cx); + element.measure(available_item_space, cx).height + }; - // state.pending_scroll_delta = px(0.); - // } else { - break; - // } + leading_overdraw += height; + measured_items.push_front(ListItem::Rendered { height }); + } else { + break; + } } + let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); + let mut cursor = old_items.cursor::(); + let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &()); + new_items.extend(measured_items, &()); + cursor.seek(&Count(measured_range.end), Bias::Right, &()); + new_items.append(cursor.suffix(&()), &()); + // Paint the visible items cx.with_content_mask(Some(ContentMask { bounds }), |cx| { let mut item_origin = bounds.origin; @@ -474,12 +474,13 @@ impl Element for List { } }); + state.items = new_items; + state.last_layout_bounds = Some(bounds); + let list_state = self.state.clone(); let height = bounds.size.height; - dbg!("scroll is being bound"); cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - dbg!("scroll dispatched!"); if phase == DispatchPhase::Bubble && bounds.contains(&event.position) && cx.was_top_layer(&event.position, cx.stacking_order()) @@ -624,7 +625,5 @@ mod test { // Scroll position should stay at the top of the list assert_eq!(state.logical_scroll_top().item_ix, 0); assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); - - panic!("We should not get here yet!") } } diff --git a/crates/gpui/src/keymap/matcher.rs b/crates/gpui/src/keymap/matcher.rs index 5410ddce06e9ca999aaee0911fd7a69d6a0e61c8..491dee6895fe391a4b93233cf6b861875deac803 100644 --- a/crates/gpui/src/keymap/matcher.rs +++ b/crates/gpui/src/keymap/matcher.rs @@ -209,7 +209,6 @@ mod tests { ); assert!(!matcher.has_pending_keystrokes()); - eprintln!("PROBLEM AREA"); // If a is prefixed, C will not be dispatched because there // was a pending binding for it assert_eq!( diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 00b17ba3c07ba6db532035c37d5bf7cdd74e9f59..11a720bf1f17aab827a581fff5f3a4b1b63b8379 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1717,7 +1717,6 @@ impl<'a> WindowContext<'a> { .mouse_listeners .remove(&event.type_id()) { - dbg!(handlers.len()); // Because handlers may add other handlers, we sort every time. handlers.sort_by(|(a, _, _), (b, _, _)| a.cmp(b)); From 8f3d79c3b811fdaa9ae6e85799006f43c7c94ae9 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 16:59:57 -0500 Subject: [PATCH 50/96] Fix file paths in stories (#4104) This PR fixes some file paths used in our stories that were still referencing the `ui2` crate. Release Notes: - N/A --- crates/ui/src/components/stories/icon_button.rs | 2 +- crates/ui/src/components/stories/toggle_button.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index ba3d5fd8660988298a38307cb8a690d1a365c7f4..4b53a2c0f230587fcce32edf5de734a41396f579 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -113,7 +113,7 @@ impl Render for IconButtonStory { StoryContainer::new( "Icon Button", - "crates/ui2/src/components/stories/icon_button.rs", + "crates/ui/src/components/stories/icon_button.rs", ) .child(StorySection::new().children(buttons)) .child( diff --git a/crates/ui/src/components/stories/toggle_button.rs b/crates/ui/src/components/stories/toggle_button.rs index da2a2512c44a7705d817a12a25b6355f7f792c04..68789a53409987c9582dd6b42027b342d1a93c97 100644 --- a/crates/ui/src/components/stories/toggle_button.rs +++ b/crates/ui/src/components/stories/toggle_button.rs @@ -9,7 +9,7 @@ impl Render for ToggleButtonStory { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { StoryContainer::new( "Toggle Button", - "crates/ui2/src/components/stories/toggle_button.rs", + "crates/ui/src/components/stories/toggle_button.rs", ) .child( StorySection::new().child( From d67e461325d530a3da8befb72670f998612cd16a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 10:00:21 -0800 Subject: [PATCH 51/96] =?UTF-8?q?document=20app=20module=20in=20gpui=20?= =?UTF-8?q?=F0=9F=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit co-authored-by: Nathan --- crates/gpui/src/app.rs | 65 +++++++++++++++++++++++++++- crates/gpui/src/app/async_context.rs | 31 +++++++++++-- crates/gpui/src/app/entity_map.rs | 25 +++++++++-- crates/gpui/src/app/model_context.rs | 17 ++++++++ 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 41519f0ae4d3623a5b3e57c06a265c5f0a754b23..ab9b4d9f86417c13127de5a8758297c8d4ce0028 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1,3 +1,5 @@ +#![deny(missing_docs)] + mod async_context; mod entity_map; mod model_context; @@ -43,6 +45,9 @@ use util::{ ResultExt, }; +/// The duration for which futures returned from [AppContext::on_app_context] or [ModelContext::on_app_quit] can run before the application fully quits. +pub const SHUTDOWN_TIMEOUT: Duration = Duration::from_millis(100); + /// Temporary(?) wrapper around [`RefCell`] to help us debug any double borrows. /// Strongly consider removing after stabilization. #[doc(hidden)] @@ -187,6 +192,9 @@ type QuitHandler = Box LocalBoxFuture<'static, () type ReleaseListener = Box; type NewViewListener = Box; +/// Contains the state of the full application, and passed as a reference to a variety of callbacks. +/// Other contexts such as [ModelContext], [WindowContext], and [ViewContext] deref to this type, making it the most general context type. +/// You need a reference to an `AppContext` to access the state of a [Model]. pub struct AppContext { pub(crate) this: Weak, pub(crate) platform: Rc, @@ -312,7 +320,7 @@ impl AppContext { let futures = futures::future::join_all(futures); if self .background_executor - .block_with_timeout(Duration::from_millis(100), futures) + .block_with_timeout(SHUTDOWN_TIMEOUT, futures) .is_err() { log::error!("timed out waiting on app_will_quit"); @@ -446,6 +454,7 @@ impl AppContext { .collect() } + /// Returns a handle to the window that is currently focused at the platform level, if one exists. pub fn active_window(&self) -> Option { self.platform.active_window() } @@ -474,14 +483,17 @@ impl AppContext { self.platform.activate(ignoring_other_apps); } + /// Hide the application at the platform level. pub fn hide(&self) { self.platform.hide(); } + /// Hide other applications at the platform level. pub fn hide_other_apps(&self) { self.platform.hide_other_apps(); } + /// Unhide other applications at the platform level. pub fn unhide_other_apps(&self) { self.platform.unhide_other_apps(); } @@ -521,18 +533,25 @@ impl AppContext { self.platform.open_url(url); } + /// Returns the full pathname of the current app bundle. + /// If the app is not being run from a bundle, returns an error. pub fn app_path(&self) -> Result { self.platform.app_path() } + /// Returns the file URL of the executable with the specified name in the application bundle pub fn path_for_auxiliary_executable(&self, name: &str) -> Result { self.platform.path_for_auxiliary_executable(name) } + /// Returns the maximum duration in which a second mouse click must occur for an event to be a double-click event. pub fn double_click_interval(&self) -> Duration { self.platform.double_click_interval() } + /// Displays a platform modal for selecting paths. + /// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel. + /// If cancelled, a `None` will be relayed instead. pub fn prompt_for_paths( &self, options: PathPromptOptions, @@ -540,22 +559,30 @@ impl AppContext { self.platform.prompt_for_paths(options) } + /// Displays a platform modal for selecting a new path where a file can be saved. + /// The provided directory will be used to set the iniital location. + /// When a path is selected, it is relayed asynchronously via the returned oneshot channel. + /// If cancelled, a `None` will be relayed instead. pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver> { self.platform.prompt_for_new_path(directory) } + /// Reveals the specified path at the platform level, such as in Finder on macOS. pub fn reveal_path(&self, path: &Path) { self.platform.reveal_path(path) } + /// Returns whether the user has configured scrollbars to auto-hide at the platform level. pub fn should_auto_hide_scrollbars(&self) -> bool { self.platform.should_auto_hide_scrollbars() } + /// Restart the application. pub fn restart(&self) { self.platform.restart() } + /// Returns the local timezone at the platform level. pub fn local_timezone(&self) -> UtcOffset { self.platform.local_timezone() } @@ -745,7 +772,7 @@ impl AppContext { } /// Spawns the future returned by the given function on the thread pool. The closure will be invoked - /// with AsyncAppContext, which allows the application state to be accessed across await points. + /// with [AsyncAppContext], which allows the application state to be accessed across await points. pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, @@ -896,6 +923,8 @@ impl AppContext { self.globals_by_type.insert(global_type, lease.global); } + /// Arrange for the given function to be invoked whenever a view of the specified type is created. + /// The function will be passed a mutable reference to the view along with an appropriate context. pub fn observe_new_views( &mut self, on_new: impl 'static + Fn(&mut V, &mut ViewContext), @@ -915,6 +944,8 @@ impl AppContext { subscription } + /// Observe the release of a model or view. The callback is invoked after the model or view + /// has no more strong references but before it has been dropped. pub fn observe_release( &mut self, handle: &E, @@ -935,6 +966,9 @@ impl AppContext { subscription } + /// Register a callback to be invoked when a keystroke is received by the application + /// in any window. Note that this fires after all other action and event mechansims have resolved + /// and that this API will not be invoked if the event's propogation is stopped. pub fn observe_keystrokes( &mut self, f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static, @@ -958,6 +992,7 @@ impl AppContext { self.pending_effects.push_back(Effect::Refresh); } + /// Clear all key bindings in the app. pub fn clear_key_bindings(&mut self) { self.keymap.lock().clear(); self.pending_effects.push_back(Effect::Refresh); @@ -992,6 +1027,7 @@ impl AppContext { self.propagate_event = true; } + /// Build an action from some arbitrary data, typically a keymap entry. pub fn build_action( &self, name: &str, @@ -1000,10 +1036,16 @@ impl AppContext { self.actions.build_action(name, data) } + /// Get a list of all action names that have been registered. + /// in the application. Note that registration only allows for + /// actions to be built dynamically, and is unrelated to binding + /// actions in the element tree. pub fn all_action_names(&self) -> &[SharedString] { self.actions.all_action_names() } + /// Register a callback to be invoked when the application is about to quit. + /// It is not possible to cancel the quit event at this point. pub fn on_app_quit( &mut self, mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static, @@ -1039,6 +1081,8 @@ impl AppContext { } } + /// Checks if the given action is bound in the current context, as defined by the app's current focus, + /// the bindings in the element tree, and any global action listeners. pub fn is_action_available(&mut self, action: &dyn Action) -> bool { if let Some(window) = self.active_window() { if let Ok(window_action_available) = @@ -1052,10 +1096,13 @@ impl AppContext { .contains_key(&action.as_any().type_id()) } + /// Set the menu bar for this application. This will replace any existing menu bar. pub fn set_menus(&mut self, menus: Vec) { self.platform.set_menus(menus, &self.keymap.lock()); } + /// Dispatch an action to the currently active window or global action handler + /// See [action::Action] for more information on how actions work pub fn dispatch_action(&mut self, action: &dyn Action) { if let Some(active_window) = self.active_window() { active_window @@ -1110,6 +1157,7 @@ impl AppContext { } } + /// Is there currently something being dragged? pub fn has_active_drag(&self) -> bool { self.active_drag.is_some() } @@ -1262,8 +1310,14 @@ impl DerefMut for GlobalLease { /// Contains state associated with an active drag operation, started by dragging an element /// within the window or by dragging into the app from the underlying platform. pub struct AnyDrag { + /// The view used to render this drag pub view: AnyView, + + /// The value of the dragged item, to be dropped pub value: Box, + + /// This is used to render the dragged item in the same place + /// on the original element that the drag was initiated pub cursor_offset: Point, } @@ -1271,12 +1325,19 @@ pub struct AnyDrag { /// tooltip behavior on a custom element. Otherwise, use [Div::tooltip]. #[derive(Clone)] pub struct AnyTooltip { + /// The view used to display the tooltip pub view: AnyView, + + /// The offset from the cursor to use, relative to the parent view pub cursor_offset: Point, } +/// A keystroke event, and potentially the associated action #[derive(Debug)] pub struct KeystrokeEvent { + /// The keystroke that occurred pub keystroke: Keystroke, + + /// The action that was resolved for the keystroke, if any pub action: Option>, } diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 6afb356e5e63c6431fe0858b4e1c4fd97159c0b2..1ee01d90dfac22632f718088bbd7bbe54364136c 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -7,6 +7,9 @@ use anyhow::{anyhow, Context as _}; use derive_more::{Deref, DerefMut}; use std::{future::Future, rc::Weak}; +/// An async-friendly version of [AppContext] with a static lifetime so it can be held across `await` points in async code. +/// You're provided with an instance when calling [AppContext::spawn], and you can also create one with [AppContext::to_async]. +/// Internally, this holds a weak reference to an `AppContext`, so its methods are fallible to protect against cases where the [AppContext] is dropped. #[derive(Clone)] pub struct AsyncAppContext { pub(crate) app: Weak, @@ -139,6 +142,8 @@ impl AsyncAppContext { self.foreground_executor.spawn(f(self.clone())) } + /// Determine whether global state of the specified type has been assigned. + /// Returns an error if the `AppContext` has been dropped. pub fn has_global(&self) -> Result { let app = self .app @@ -148,6 +153,9 @@ impl AsyncAppContext { Ok(app.has_global::()) } + /// Reads the global state of the specified type, passing it to the given callback. + /// Panics if no global state of the specified type has been assigned. + /// Returns an error if the `AppContext` has been dropped. pub fn read_global(&self, read: impl FnOnce(&G, &AppContext) -> R) -> Result { let app = self .app @@ -157,6 +165,9 @@ impl AsyncAppContext { Ok(read(app.global(), &app)) } + /// Reads the global state of the specified type, passing it to the given callback. + /// Similar to [read_global], but returns an error instead of panicking if no state of the specified type has been assigned. + /// Returns an error if no state of the specified type has been assigned the `AppContext` has been dropped. pub fn try_read_global( &self, read: impl FnOnce(&G, &AppContext) -> R, @@ -166,6 +177,8 @@ impl AsyncAppContext { Some(read(app.try_global()?, &app)) } + /// A convenience method for [AppContext::update_global] + /// for updating the global state of the specified type. pub fn update_global( &mut self, update: impl FnOnce(&mut G, &mut AppContext) -> R, @@ -179,6 +192,8 @@ impl AsyncAppContext { } } +/// A cloneable, owned handle to the application context, +/// composed with the window associated with the current task. #[derive(Clone, Deref, DerefMut)] pub struct AsyncWindowContext { #[deref] @@ -188,14 +203,16 @@ pub struct AsyncWindowContext { } impl AsyncWindowContext { - pub fn window_handle(&self) -> AnyWindowHandle { - self.window - } - pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self { Self { app, window } } + /// Get the handle of the window this context is associated with. + pub fn window_handle(&self) -> AnyWindowHandle { + self.window + } + + /// A convenience method for [WindowContext::update()] pub fn update( &mut self, update: impl FnOnce(AnyView, &mut WindowContext) -> R, @@ -203,10 +220,12 @@ impl AsyncWindowContext { self.app.update_window(self.window, update) } + /// A convenience method for [WindowContext::on_next_frame()] pub fn on_next_frame(&mut self, f: impl FnOnce(&mut WindowContext) + 'static) { self.window.update(self, |_, cx| cx.on_next_frame(f)).ok(); } + /// A convenience method for [AppContext::global()] pub fn read_global( &mut self, read: impl FnOnce(&G, &WindowContext) -> R, @@ -214,6 +233,8 @@ impl AsyncWindowContext { self.window.update(self, |_, cx| read(cx.global(), cx)) } + /// A convenience method for [AppContext::update_global()] + /// for updating the global state of the specified type. pub fn update_global( &mut self, update: impl FnOnce(&mut G, &mut WindowContext) -> R, @@ -224,6 +245,8 @@ impl AsyncWindowContext { self.window.update(self, |_, cx| cx.update_global(update)) } + /// Schedule a future to be executed on the main thread. This is used for collecting + /// the results of background tasks and updating the UI. pub fn spawn(&self, f: impl FnOnce(AsyncWindowContext) -> Fut) -> Task where Fut: Future + 'static, diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 1e593caf98a34f64939b6253fbb1eccd0244bfb3..d3f8a7ea894991822cc9764627f4a77bb0fa28f6 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -31,6 +31,7 @@ impl From for EntityId { } impl EntityId { + /// Converts this entity id to a [u64] pub fn as_u64(self) -> u64 { self.0.as_ffi() } @@ -140,7 +141,7 @@ impl EntityMap { } } -pub struct Lease<'a, T> { +pub(crate) struct Lease<'a, T> { entity: Option>, pub model: &'a Model, entity_type: PhantomData, @@ -169,8 +170,9 @@ impl<'a, T> Drop for Lease<'a, T> { } #[derive(Deref, DerefMut)] -pub struct Slot(Model); +pub(crate) struct Slot(Model); +/// A dynamically typed reference to a model, which can be downcast into a `Model`. pub struct AnyModel { pub(crate) entity_id: EntityId, pub(crate) entity_type: TypeId, @@ -195,14 +197,17 @@ impl AnyModel { } } + /// Returns the id associated with this model. pub fn entity_id(&self) -> EntityId { self.entity_id } + /// Returns the [TypeId] associated with this model. pub fn entity_type(&self) -> TypeId { self.entity_type } + /// Converts this model handle into a weak variant, which does not prevent it from being released. pub fn downgrade(&self) -> AnyWeakModel { AnyWeakModel { entity_id: self.entity_id, @@ -211,6 +216,8 @@ impl AnyModel { } } + /// Converts this model handle into a strongly-typed model handle of the given type. + /// If this model handle is not of the specified type, returns itself as an error variant. pub fn downcast(self) -> Result, AnyModel> { if TypeId::of::() == self.entity_type { Ok(Model { @@ -307,6 +314,8 @@ impl std::fmt::Debug for AnyModel { } } +/// A strong, well typed reference to a struct which is managed +/// by GPUI #[derive(Deref, DerefMut)] pub struct Model { #[deref] @@ -368,10 +377,12 @@ impl Model { self.any_model } + /// Grab a reference to this entity from the context. pub fn read<'a>(&self, cx: &'a AppContext) -> &'a T { cx.entities.read(self) } + /// Read the entity referenced by this model with the given function. pub fn read_with( &self, cx: &C, @@ -437,6 +448,7 @@ impl PartialEq> for Model { } } +/// A type erased, weak reference to a model. #[derive(Clone)] pub struct AnyWeakModel { pub(crate) entity_id: EntityId, @@ -445,10 +457,12 @@ pub struct AnyWeakModel { } impl AnyWeakModel { + /// Get the entity ID associated with this weak reference. pub fn entity_id(&self) -> EntityId { self.entity_id } + /// Check if this weak handle can be upgraded, or if the model has already been dropped pub fn is_upgradable(&self) -> bool { let ref_count = self .entity_ref_counts @@ -458,6 +472,7 @@ impl AnyWeakModel { ref_count > 0 } + /// Upgrade this weak model reference to a strong reference. pub fn upgrade(&self) -> Option { let ref_counts = &self.entity_ref_counts.upgrade()?; let ref_counts = ref_counts.read(); @@ -485,6 +500,7 @@ impl AnyWeakModel { }) } + /// Assert that model referenced by this weak handle has been dropped. #[cfg(any(test, feature = "test-support"))] pub fn assert_dropped(&self) { self.entity_ref_counts @@ -527,6 +543,7 @@ impl PartialEq for AnyWeakModel { impl Eq for AnyWeakModel {} +/// A weak reference to a model of the given type. #[derive(Deref, DerefMut)] pub struct WeakModel { #[deref] @@ -617,12 +634,12 @@ lazy_static::lazy_static! { #[cfg(any(test, feature = "test-support"))] #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] -pub struct HandleId { +pub(crate) struct HandleId { id: u64, // id of the handle itself, not the pointed at object } #[cfg(any(test, feature = "test-support"))] -pub struct LeakDetector { +pub(crate) struct LeakDetector { next_handle_id: u64, entity_handles: HashMap>>, } diff --git a/crates/gpui/src/app/model_context.rs b/crates/gpui/src/app/model_context.rs index 63db0ee1cb1c2e28a11268e5d1df8b74189954b3..e2aad9fee93aeba5c0c022a5d619f8cdd57b0e63 100644 --- a/crates/gpui/src/app/model_context.rs +++ b/crates/gpui/src/app/model_context.rs @@ -11,6 +11,7 @@ use std::{ future::Future, }; +/// The app context, with specialized behavior for the given model. #[derive(Deref, DerefMut)] pub struct ModelContext<'a, T> { #[deref] @@ -24,20 +25,24 @@ impl<'a, T: 'static> ModelContext<'a, T> { Self { app, model_state } } + /// The entity id of the model backing this context. pub fn entity_id(&self) -> EntityId { self.model_state.entity_id } + /// Returns a handle to the model belonging to this context. pub fn handle(&self) -> Model { self.weak_model() .upgrade() .expect("The entity must be alive if we have a model context") } + /// Returns a weak handle to the model belonging to this context. pub fn weak_model(&self) -> WeakModel { self.model_state.clone() } + /// Arranges for the given function to be called whenever [ModelContext::notify] or [ViewContext::notify] is called with the given model or view. pub fn observe( &mut self, entity: &E, @@ -59,6 +64,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { }) } + /// Subscribe to an event type from another model or view pub fn subscribe( &mut self, entity: &E, @@ -81,6 +87,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { }) } + /// Register a callback to be invoked when GPUI releases this model. pub fn on_release( &mut self, on_release: impl FnOnce(&mut T, &mut AppContext) + 'static, @@ -99,6 +106,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { subscription } + /// Register a callback to be run on the release of another model or view pub fn observe_release( &mut self, entity: &E, @@ -124,6 +132,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { subscription } + /// Register a callback to for updates to the given global pub fn observe_global( &mut self, mut f: impl FnMut(&mut T, &mut ModelContext<'_, T>) + 'static, @@ -140,6 +149,8 @@ impl<'a, T: 'static> ModelContext<'a, T> { subscription } + /// Arrange for the given function to be invoked whenever the application is quit. + /// The future returned from this callback will be polled for up to [gpui::SHUTDOWN_TIMEOUT] until the app fully quits. pub fn on_app_quit( &mut self, mut on_quit: impl FnMut(&mut T, &mut ModelContext) -> Fut + 'static, @@ -165,6 +176,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { subscription } + /// Tell GPUI that this model has changed and observers of it should be notified. pub fn notify(&mut self) { if self .app @@ -177,6 +189,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { } } + /// Update the given global pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R where G: 'static, @@ -187,6 +200,9 @@ impl<'a, T: 'static> ModelContext<'a, T> { result } + /// Spawn the future returned by the given function. + /// The function is provided a weak handle to the model owned by this context and a context that can be held across await points. + /// The returned task must be held or detached. pub fn spawn(&self, f: impl FnOnce(WeakModel, AsyncAppContext) -> Fut) -> Task where T: 'static, @@ -199,6 +215,7 @@ impl<'a, T: 'static> ModelContext<'a, T> { } impl<'a, T> ModelContext<'a, T> { + /// Emit an event of the specified type, which can be handled by other entities that have subscribed via `subscribe` methods on their respective contexts. pub fn emit(&mut self, event: Evt) where T: EventEmitter, From 7a299e966afeb9eb5f0940ad9f22cdfcaa886a4c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 10:21:29 -0800 Subject: [PATCH 52/96] Document view crate co-authored-by: Nathan --- crates/gpui/src/view.rs | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 968fbbd94cd142bdfc6629539da997652f71d91c..3701bbbd69419daff04e83fdc85413099d34f42f 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -1,3 +1,5 @@ +#![deny(missing_docs)] + use crate::{ seal::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, @@ -11,12 +13,16 @@ use std::{ hash::{Hash, Hasher}, }; +/// A view is a piece of state that can be presented on screen by implementing the [Render] trait. +/// Views implement [Element] and can composed with other views, and every window is created with a root view. pub struct View { + /// A view is just a [Model] whose type implements `Render`, and the model is accessible via this field. pub model: Model, } impl Sealed for View {} +#[doc(hidden)] pub struct AnyViewState { root_style: Style, cache_key: Option, @@ -58,6 +64,7 @@ impl View { Entity::downgrade(self) } + /// Update the view's state with the given function, which is passed a mutable reference and a context. pub fn update( &self, cx: &mut C, @@ -69,10 +76,12 @@ impl View { cx.update_view(self, f) } + /// Obtain a read-only reference to this view's state. pub fn read<'a>(&self, cx: &'a AppContext) -> &'a V { self.model.read(cx) } + /// Gets a [FocusHandle] for this view when its state implements [FocusableView]. pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle where V: FocusableView, @@ -131,19 +140,24 @@ impl PartialEq for View { impl Eq for View {} +/// A weak variant of [View] which does not prevent the view from being released. pub struct WeakView { pub(crate) model: WeakModel, } impl WeakView { + /// Gets the entity id associated with this handle. pub fn entity_id(&self) -> EntityId { self.model.entity_id } + /// Obtain a strong handle for the view if it hasn't been released. pub fn upgrade(&self) -> Option> { Entity::upgrade_from(self) } + /// Update this view's state if it hasn't been released. + /// Returns an error if this view has been released. pub fn update( &self, cx: &mut C, @@ -157,9 +171,10 @@ impl WeakView { Ok(view.update(cx, f)).flatten() } + /// Assert that the view referenced by this handle has been released. #[cfg(any(test, feature = "test-support"))] - pub fn assert_dropped(&self) { - self.model.assert_dropped() + pub fn assert_released(&self) { + self.model.assert_released() } } @@ -185,6 +200,7 @@ impl PartialEq for WeakView { impl Eq for WeakView {} +/// A dynically-typed handle to a view, which can be downcast to a [View] for a specific type. #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, @@ -193,11 +209,15 @@ pub struct AnyView { } impl AnyView { + /// Indicate that this view should be cached when using it as an element. + /// When using this method, the view's previous layout and paint will be recycled from the previous frame if [ViewContext::notify] has not been called since it was rendered. + /// The one exception is when [WindowContext::refresh] is called, in which case caching is ignored. pub fn cached(mut self) -> Self { self.cache = true; self } + /// Convert this to a weak handle. pub fn downgrade(&self) -> AnyWeakView { AnyWeakView { model: self.model.downgrade(), @@ -205,6 +225,8 @@ impl AnyView { } } + /// Convert this to a [View] of a specific type. + /// If this handle does not contain a view of the specified type, returns itself in an `Err` variant. pub fn downcast(self) -> Result, Self> { match self.model.downcast() { Ok(model) => Ok(View { model }), @@ -216,10 +238,12 @@ impl AnyView { } } + /// Gets the [TypeId] of the underlying view. pub fn entity_type(&self) -> TypeId { self.model.entity_type } + /// Gets the entity id of this handle. pub fn entity_id(&self) -> EntityId { self.model.entity_id() } @@ -337,12 +361,14 @@ impl IntoElement for AnyView { } } +/// A weak, dynamically-typed view handle that does not prevent the view from being released. pub struct AnyWeakView { model: AnyWeakModel, layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), } impl AnyWeakView { + /// Convert to a strongly-typed handle if the referenced view has not yet been released. pub fn upgrade(&self) -> Option { let model = self.model.upgrade()?; Some(AnyView { From 9eecda2dae1633568285adc27d37a2de2408120c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 10:23:46 -0800 Subject: [PATCH 53/96] Update method name and partially document platform crate co-authored-by: Nathan --- crates/collab/src/tests/following_tests.rs | 2 +- crates/gpui/src/app/entity_map.rs | 12 +++++----- crates/gpui/src/executor.rs | 3 ++- crates/gpui/src/platform.rs | 26 ++++++++++++++++----- crates/gpui/src/platform/mac/display.rs | 5 ---- crates/gpui/src/platform/test/display.rs | 4 ---- crates/gpui/src/scene.rs | 8 +++---- crates/gpui/src/text_system.rs | 2 +- crates/gpui/src/text_system/line_wrapper.rs | 2 +- crates/zed/src/zed.rs | 6 ++--- 10 files changed, 38 insertions(+), 32 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index dc5488ebb335ba0e1ecce9800a7c689d217d5a0e..af184d7d02a3458deddb7bfdbf1f77be48d70790 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -249,7 +249,7 @@ async fn test_basic_following( executor.run_until_parked(); cx_c.cx.update(|_| {}); - weak_workspace_c.assert_dropped(); + weak_workspace_c.assert_released(); // Clients A and B see that client B is following A, and client C is not present in the followers. executor.run_until_parked(); diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index d3f8a7ea894991822cc9764627f4a77bb0fa28f6..7ab21a5477526d064d70283a04442b33cffcedfa 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -281,7 +281,7 @@ impl Drop for AnyModel { entity_map .write() .leak_detector - .handle_dropped(self.entity_id, self.handle_id) + .handle_released(self.entity_id, self.handle_id) } } } @@ -500,15 +500,15 @@ impl AnyWeakModel { }) } - /// Assert that model referenced by this weak handle has been dropped. + /// Assert that model referenced by this weak handle has been released. #[cfg(any(test, feature = "test-support"))] - pub fn assert_dropped(&self) { + pub fn assert_released(&self) { self.entity_ref_counts .upgrade() .unwrap() .write() .leak_detector - .assert_dropped(self.entity_id); + .assert_released(self.entity_id); if self .entity_ref_counts @@ -658,12 +658,12 @@ impl LeakDetector { handle_id } - pub fn handle_dropped(&mut self, entity_id: EntityId, handle_id: HandleId) { + pub fn handle_released(&mut self, entity_id: EntityId, handle_id: HandleId) { let handles = self.entity_handles.entry(entity_id).or_default(); handles.remove(&handle_id); } - pub fn assert_dropped(&mut self, entity_id: EntityId) { + pub fn assert_released(&mut self, entity_id: EntityId) { let handles = self.entity_handles.entry(entity_id).or_default(); if !handles.is_empty() { for (_, backtrace) in handles { diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index fc60cb1ec6afcd1c79f5b561a436eac4635c47bc..8571c1ee57adf49d0b91166a31687b8612d10da3 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -109,9 +109,10 @@ type AnyFuture = Pin>>; /// BackgroundExecutor lets you run things on background threads. /// In production this is a thread pool with no ordering guarantees. -/// In tests this is simalated by running tasks one by one in a deterministic +/// In tests this is simulated by running tasks one by one in a deterministic /// (but arbitrary) order controlled by the `SEED` environment variable. impl BackgroundExecutor { + #[doc(hidden)] pub fn new(dispatcher: Arc) -> Self { Self { dispatcher } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f165cd9c2b5aa71a3a982a5e23faafaca2b8bf3a..7b260f1a7d1aa56d27e52bdd2d0d30501a5d8c9c 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -114,15 +114,20 @@ pub(crate) trait Platform: 'static { fn delete_credentials(&self, url: &str) -> Result<()>; } +/// A handle to a platform's display, e.g. a monitor or laptop screen. pub trait PlatformDisplay: Send + Sync + Debug { + /// Get the ID for this display fn id(&self) -> DisplayId; + /// Returns a stable identifier for this display that can be persisted and used /// across system restarts. fn uuid(&self) -> Result; - fn as_any(&self) -> &dyn Any; + + /// Get the bounds for this display fn bounds(&self) -> Bounds; } +/// An opaque identifier for a hardware display #[derive(PartialEq, Eq, Hash, Copy, Clone)] pub struct DisplayId(pub(crate) u32); @@ -134,7 +139,7 @@ impl Debug for DisplayId { unsafe impl Send for DisplayId {} -pub trait PlatformWindow { +pub(crate) trait PlatformWindow { fn bounds(&self) -> WindowBounds; fn content_size(&self) -> Size; fn scale_factor(&self) -> f32; @@ -175,6 +180,9 @@ pub trait PlatformWindow { } } +/// This type is public so that our test macro can generate and use it, but it should not +/// be considered part of our public API. +#[doc(hidden)] pub trait PlatformDispatcher: Send + Sync { fn is_main_thread(&self) -> bool; fn dispatch(&self, runnable: Runnable, label: Option); @@ -190,7 +198,7 @@ pub trait PlatformDispatcher: Send + Sync { } } -pub trait PlatformTextSystem: Send + Sync { +pub(crate) trait PlatformTextSystem: Send + Sync { fn add_fonts(&self, fonts: &[Arc>]) -> Result<()>; fn all_font_names(&self) -> Vec; fn font_id(&self, descriptor: &Font) -> Result; @@ -214,15 +222,21 @@ pub trait PlatformTextSystem: Send + Sync { ) -> Vec; } +/// Basic metadata about the current application and operating system. #[derive(Clone, Debug)] pub struct AppMetadata { + /// The name of the current operating system pub os_name: &'static str, + + /// The operating system's version pub os_version: Option, + + /// The current version of the application pub app_version: Option, } #[derive(PartialEq, Eq, Hash, Clone)] -pub enum AtlasKey { +pub(crate) enum AtlasKey { Glyph(RenderGlyphParams), Svg(RenderSvgParams), Image(RenderImageParams), @@ -262,7 +276,7 @@ impl From for AtlasKey { } } -pub trait PlatformAtlas: Send + Sync { +pub(crate) trait PlatformAtlas: Send + Sync { fn get_or_insert_with<'a>( &self, key: &AtlasKey, @@ -274,7 +288,7 @@ pub trait PlatformAtlas: Send + Sync { #[derive(Clone, Debug, PartialEq, Eq)] #[repr(C)] -pub struct AtlasTile { +pub(crate) struct AtlasTile { pub(crate) texture_id: AtlasTextureId, pub(crate) tile_id: TileId, pub(crate) bounds: Bounds, diff --git a/crates/gpui/src/platform/mac/display.rs b/crates/gpui/src/platform/mac/display.rs index 2b72c335c80e80f700d6caa25fd64d5c8bf4f414..95ec83cd5a9d120fbcbe531a5cec3c9dafa3c95a 100644 --- a/crates/gpui/src/platform/mac/display.rs +++ b/crates/gpui/src/platform/mac/display.rs @@ -11,7 +11,6 @@ use core_graphics::{ geometry::{CGPoint, CGRect, CGSize}, }; use objc::{msg_send, sel, sel_impl}; -use std::any::Any; use uuid::Uuid; #[derive(Debug)] @@ -154,10 +153,6 @@ impl PlatformDisplay for MacDisplay { ])) } - fn as_any(&self) -> &dyn Any { - self - } - fn bounds(&self) -> Bounds { unsafe { let native_bounds = CGDisplayBounds(self.0); diff --git a/crates/gpui/src/platform/test/display.rs b/crates/gpui/src/platform/test/display.rs index 68dbb0fdf3466b9f3a2800bf68768aea2f5dd1e2..838d600147b86e62d2f8197cf695590dbdefd750 100644 --- a/crates/gpui/src/platform/test/display.rs +++ b/crates/gpui/src/platform/test/display.rs @@ -31,10 +31,6 @@ impl PlatformDisplay for TestDisplay { Ok(self.uuid) } - fn as_any(&self) -> &dyn std::any::Any { - unimplemented!() - } - fn bounds(&self) -> crate::Bounds { self.bounds } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index de031704cd2888917534083bffd90e00c7e8b49b..b69c10c752296cb6ef765fc924ae030d46a450b4 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -93,7 +93,7 @@ impl Scene { } } - pub fn insert(&mut self, order: &StackingOrder, primitive: impl Into) { + pub(crate) fn insert(&mut self, order: &StackingOrder, primitive: impl Into) { let primitive = primitive.into(); let clipped_bounds = primitive .bounds() @@ -440,7 +440,7 @@ pub enum PrimitiveKind { Surface, } -pub enum Primitive { +pub(crate) enum Primitive { Shadow(Shadow), Quad(Quad), Path(Path), @@ -589,7 +589,7 @@ impl From for Primitive { #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] -pub struct MonochromeSprite { +pub(crate) struct MonochromeSprite { pub view_id: ViewId, pub layer_id: LayerId, pub order: DrawOrder, @@ -622,7 +622,7 @@ impl From for Primitive { #[derive(Clone, Debug, Eq, PartialEq)] #[repr(C)] -pub struct PolychromeSprite { +pub(crate) struct PolychromeSprite { pub view_id: ViewId, pub layer_id: LayerId, pub order: DrawOrder, diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 34470aff021297d9b8a12025e5348550cd6111c5..27d216dd50e673659e9873908d7a8f54364a256f 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -47,7 +47,7 @@ pub struct TextSystem { } impl TextSystem { - pub fn new(platform_text_system: Arc) -> Self { + pub(crate) fn new(platform_text_system: Arc) -> Self { TextSystem { line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())), platform_text_system, diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index f6963dbfd4ed6dec3c467741a45d6c5531aa0788..1c5b2a8f993324eb031a3e6c04c227cf310c1011 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -13,7 +13,7 @@ pub struct LineWrapper { impl LineWrapper { pub const MAX_INDENT: u32 = 256; - pub fn new( + pub(crate) fn new( font_id: FontId, font_size: Pixels, text_system: Arc, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index bbe5e781094e33e8a53845d699e50ecd21552871..afc06ad193659e7c015f2039f2efaa0f95a3297a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1809,9 +1809,9 @@ mod tests { assert!(workspace.active_item(cx).is_none()); }) .unwrap(); - editor_1.assert_dropped(); - editor_2.assert_dropped(); - buffer.assert_dropped(); + editor_1.assert_released(); + editor_2.assert_released(); + buffer.assert_released(); } #[gpui::test] From 57400e9687da088d8a55515926c11a5328fd8571 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 14:31:21 -0800 Subject: [PATCH 54/96] Fix typos detected by crate-ci/typos --- assets/themes/src/vscode/dracula/dracula.json | 2 +- .../src/vscode/night-owl/night-owl-light.json | 6 +++--- .../themes/src/vscode/night-owl/night-owl.json | 6 +++--- assets/themes/src/vscode/noctis/azureus.json | 2 +- assets/themes/src/vscode/noctis/bordo.json | 2 +- assets/themes/src/vscode/noctis/hibernus.json | 2 +- assets/themes/src/vscode/noctis/lilac.json | 2 +- assets/themes/src/vscode/noctis/lux.json | 2 +- assets/themes/src/vscode/noctis/minimus.json | 2 +- assets/themes/src/vscode/noctis/noctis.json | 2 +- assets/themes/src/vscode/noctis/obscuro.json | 2 +- assets/themes/src/vscode/noctis/sereno.json | 2 +- assets/themes/src/vscode/noctis/uva.json | 2 +- assets/themes/src/vscode/noctis/viola.json | 2 +- .../palenight/palenight-mild-contrast.json | 10 +++++----- .../src/vscode/palenight/palenight-operator.json | 10 +++++----- .../themes/src/vscode/palenight/palenight.json | 10 +++++----- crates/ai/src/prompts/base.rs | 10 +++++----- crates/ai/src/prompts/repository_context.rs | 6 +++--- crates/ai/src/providers/open_ai/completion.rs | 2 +- crates/assistant/src/assistant_panel.rs | 2 +- crates/collab/src/db/queries/buffers.rs | 4 ++-- crates/collab/src/tests/channel_tests.rs | 2 +- crates/collab/src/tests/editor_tests.rs | 10 +++++----- .../collab_ui/src/collab_panel/channel_modal.rs | 4 ++-- crates/copilot_ui/src/sign_in.rs | 4 ++-- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/inlay_hint_cache.rs | 12 ++++++------ crates/fs/src/repository.rs | 2 +- crates/gpui/src/executor.rs | 4 ++-- crates/gpui/src/keymap/matcher.rs | 4 ++-- crates/gpui/src/platform/keystroke.rs | 2 +- crates/gpui/src/platform/mac.rs | 2 +- ...window_appearence.rs => window_appearance.rs} | 0 crates/gpui/src/style.rs | 6 +++--- crates/gpui/src/window.rs | 2 +- crates/gpui_macros/src/gpui_macros.rs | 2 +- crates/language/src/buffer_tests.rs | 2 +- crates/lsp/src/lsp.rs | 2 +- crates/multi_buffer/src/multi_buffer.rs | 16 ++++++++-------- crates/project/src/lsp_command.rs | 2 +- crates/project/src/project.rs | 4 ++-- crates/project/src/project_settings.rs | 2 +- crates/project_panel/src/file_associations.rs | 2 +- crates/recent_projects/src/recent_projects.rs | 2 +- .../derive_refineable/src/derive_refineable.rs | 12 ++++++------ crates/search/src/project_search.rs | 2 +- crates/semantic_index/README.md | 2 +- crates/semantic_index/src/parsing.rs | 2 +- .../semantic_index/src/semantic_index_tests.rs | 8 ++++---- crates/sqlez/src/migrations.rs | 2 +- crates/storybook/src/stories/text.rs | 4 ++-- crates/terminal_view/src/terminal_view.rs | 4 ++-- crates/theme/src/styles/syntax.rs | 2 +- crates/theme_importer/src/main.rs | 4 ++-- crates/ui/src/components/button/button_like.rs | 2 +- crates/ui/src/components/popover.rs | 2 +- crates/ui/src/styles/elevation.rs | 2 +- crates/vcs_menu/src/lib.rs | 2 +- crates/vim/src/normal/repeat.rs | 2 +- crates/vim/src/normal/search.rs | 6 +++--- crates/vim/src/test/neovim_connection.rs | 2 +- crates/vim/src/visual.rs | 8 ++++---- crates/welcome/src/base_keymap_setting.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- docs/old/building-zed.md | 2 +- docs/old/zed/syntax-highlighting.md | 2 +- docs/src/configuring_zed.md | 2 +- docs/src/developing_zed__adding_languages.md | 2 +- docs/src/developing_zed__building_zed.md | 2 +- 71 files changed, 133 insertions(+), 133 deletions(-) rename crates/gpui/src/platform/mac/{window_appearence.rs => window_appearance.rs} (100%) diff --git a/assets/themes/src/vscode/dracula/dracula.json b/assets/themes/src/vscode/dracula/dracula.json index 6604a094d5a194f74d378d25c38fb47a3f29c539..e9a29dec179baf2e518e6e82519adafe01ec879d 100644 --- a/assets/themes/src/vscode/dracula/dracula.json +++ b/assets/themes/src/vscode/dracula/dracula.json @@ -1024,7 +1024,7 @@ } }, { - "name": "SCSS attibute selector strings", + "name": "SCSS attribute selector strings", "scope": ["meta.attribute-selector.scss"], "settings": { "foreground": "#F1FA8C" diff --git a/assets/themes/src/vscode/night-owl/night-owl-light.json b/assets/themes/src/vscode/night-owl/night-owl-light.json index 81e0fc0092279aec3298ff2f77a81137e0340a68..627a55ac62b641b4b112fdf9388f6626fe64a018 100644 --- a/assets/themes/src/vscode/night-owl/night-owl-light.json +++ b/assets/themes/src/vscode/night-owl/night-owl-light.json @@ -892,14 +892,14 @@ } }, { - "name": "CoffeScript Variable Assignment", + "name": "CoffeeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#31e1eb" } }, { - "name": "CoffeScript Parameter Function", + "name": "CoffeeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#403f53" @@ -1708,7 +1708,7 @@ "keyword.operator.type", "keyword.operator", "keyword", - "punctuation.definintion.string", + "punctuation.definition.string", "punctuation", "variable.other.readwrite.js", "storage.type", diff --git a/assets/themes/src/vscode/night-owl/night-owl.json b/assets/themes/src/vscode/night-owl/night-owl.json index 6d41b6299b4b911ff5d9952e56255a090c981704..b16c22fb6afc415b17e6f78873b22ce2844eed5a 100644 --- a/assets/themes/src/vscode/night-owl/night-owl.json +++ b/assets/themes/src/vscode/night-owl/night-owl.json @@ -926,14 +926,14 @@ } }, { - "name": "CoffeScript Variable Assignment", + "name": "CoffeeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#31e1eb" } }, { - "name": "CoffeScript Parameter Function", + "name": "CoffeeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#d6deeb" @@ -1817,7 +1817,7 @@ "keyword.operator.type", "keyword.operator", "keyword", - "punctuation.definintion.string", + "punctuation.definition.string", "punctuation", "variable.other.readwrite.js", "storage.type", diff --git a/assets/themes/src/vscode/noctis/azureus.json b/assets/themes/src/vscode/noctis/azureus.json index d550e74811b63e329f46142f6c5bff6e51584b61..300113a59d0877077534d362c6f5359ccc3e8c48 100644 --- a/assets/themes/src/vscode/noctis/azureus.json +++ b/assets/themes/src/vscode/noctis/azureus.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/bordo.json b/assets/themes/src/vscode/noctis/bordo.json index a6c4853c3b078e7373f69ae1084ab7a9d5c47784..21c8a13511557dfa4cf09fd608af8b0f684ca7ae 100644 --- a/assets/themes/src/vscode/noctis/bordo.json +++ b/assets/themes/src/vscode/noctis/bordo.json @@ -389,7 +389,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/hibernus.json b/assets/themes/src/vscode/noctis/hibernus.json index a20a19289ea539b675cb42b3480eb6ff57e90e53..a2870e39058ad6ac9ecd722b28dde0823dafd926 100644 --- a/assets/themes/src/vscode/noctis/hibernus.json +++ b/assets/themes/src/vscode/noctis/hibernus.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/lilac.json b/assets/themes/src/vscode/noctis/lilac.json index 26e0fe422376496a3a91cc7d191a7057dc986073..a54b4e3c50de40e6a06b58213e9b19eaed00953f 100644 --- a/assets/themes/src/vscode/noctis/lilac.json +++ b/assets/themes/src/vscode/noctis/lilac.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/lux.json b/assets/themes/src/vscode/noctis/lux.json index 1f72b0e59cab91cb2255ee1438ace7b0102dfbcf..34dc89460e20e332a3e2eaee82ea2dec518a78c1 100644 --- a/assets/themes/src/vscode/noctis/lux.json +++ b/assets/themes/src/vscode/noctis/lux.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/minimus.json b/assets/themes/src/vscode/noctis/minimus.json index 88493d99d5993b6d72ef9a1a81228b8a82fe54c3..a347af76601a975f60ebf2f6c47a7f7d641f7885 100644 --- a/assets/themes/src/vscode/noctis/minimus.json +++ b/assets/themes/src/vscode/noctis/minimus.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/noctis.json b/assets/themes/src/vscode/noctis/noctis.json index cc270fe526f10f3f1fea30464390f1d8d5a76c8e..61e90c46a9a37752743052edb6b7decb44e3871a 100644 --- a/assets/themes/src/vscode/noctis/noctis.json +++ b/assets/themes/src/vscode/noctis/noctis.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/obscuro.json b/assets/themes/src/vscode/noctis/obscuro.json index 26d1a02de84a4bad2056444302b5e9d83faa8312..97e6f2d71a63b82bc688374f87e5333a5137d09c 100644 --- a/assets/themes/src/vscode/noctis/obscuro.json +++ b/assets/themes/src/vscode/noctis/obscuro.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/sereno.json b/assets/themes/src/vscode/noctis/sereno.json index 05768aff356e40078973f34650143e9089e1971d..b81da1edcecf18a3a4d52a58c18af2986c172485 100644 --- a/assets/themes/src/vscode/noctis/sereno.json +++ b/assets/themes/src/vscode/noctis/sereno.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/uva.json b/assets/themes/src/vscode/noctis/uva.json index 6ccbff372b8a965d9451279380f07f069b8f8f67..d4139faaf3a5c14356106d0b0da43501cfc554fd 100644 --- a/assets/themes/src/vscode/noctis/uva.json +++ b/assets/themes/src/vscode/noctis/uva.json @@ -389,7 +389,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/viola.json b/assets/themes/src/vscode/noctis/viola.json index 4d474ad31173c6e8e5888faa44e501cbc0e95aaa..889d2dfc2a96e7707b2a72150f262fa14a61f548 100644 --- a/assets/themes/src/vscode/noctis/viola.json +++ b/assets/themes/src/vscode/noctis/viola.json @@ -389,7 +389,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fileds", + "keyword.other.class.fields", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/palenight/palenight-mild-contrast.json b/assets/themes/src/vscode/palenight/palenight-mild-contrast.json index 7533d90ffd5752e5ea160ab2c686a2173aa9e4eb..598a186692c928e5c7cfaabc1f1c073aea74d303 100644 --- a/assets/themes/src/vscode/palenight/palenight-mild-contrast.json +++ b/assets/themes/src/vscode/palenight/palenight-mild-contrast.json @@ -797,14 +797,14 @@ } }, { - "name": "CoffeScript Variable Assignment", + "name": "CoffeeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#89DDFF" } }, { - "name": "CoffeScript Parameter Function", + "name": "CoffeeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#bfc7d5" @@ -1523,14 +1523,14 @@ } }, { - "name": "handlebars enitity attribute names", + "name": "handlebars entity attribute names", "scope": "entity.other.attribute-name.handlebars", "settings": { "foreground": "#89DDFF" } }, { - "name": "handlebars enitity attribute values", + "name": "handlebars entity attribute values", "scope": "entity.other.attribute-value.handlebars variable.parameter.handlebars", "settings": { "foreground": "#7986E7" @@ -1558,7 +1558,7 @@ "keyword.operator.expression.in", "keyword.operator.type", "punctuation.section.embedded.js", - "punctuation.definintion.string", + "punctuation.definition.string", "punctuation" ], "settings": { diff --git a/assets/themes/src/vscode/palenight/palenight-operator.json b/assets/themes/src/vscode/palenight/palenight-operator.json index 450d36cb9ae1233086847429ec795d5ff8e41a9f..635a2ff7607c455cef191222fc5f13fb8c63024b 100644 --- a/assets/themes/src/vscode/palenight/palenight-operator.json +++ b/assets/themes/src/vscode/palenight/palenight-operator.json @@ -797,14 +797,14 @@ } }, { - "name": "CoffeScript Variable Assignment", + "name": "CoffeeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#89DDFF" } }, { - "name": "CoffeScript Parameter Function", + "name": "CoffeeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#bfc7d5" @@ -1523,14 +1523,14 @@ } }, { - "name": "handlebars enitity attribute names", + "name": "handlebars entity attribute names", "scope": "entity.other.attribute-name.handlebars", "settings": { "foreground": "#89DDFF" } }, { - "name": "handlebars enitity attribute values", + "name": "handlebars entity attribute values", "scope": "entity.other.attribute-value.handlebars variable.parameter.handlebars", "settings": { "foreground": "#7986E7" @@ -1558,7 +1558,7 @@ "keyword.operator.expression.in", "keyword.operator.type", "punctuation.section.embedded.js", - "punctuation.definintion.string", + "punctuation.definition.string", "punctuation" ], "settings": { diff --git a/assets/themes/src/vscode/palenight/palenight.json b/assets/themes/src/vscode/palenight/palenight.json index cfbf2f8788c13cc66abfeccf9b0d619416fb642b..5cf68749f42ad9d0b81cfaf06cb8639cb8d3e717 100644 --- a/assets/themes/src/vscode/palenight/palenight.json +++ b/assets/themes/src/vscode/palenight/palenight.json @@ -797,14 +797,14 @@ } }, { - "name": "CoffeScript Variable Assignment", + "name": "CoffeeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#89DDFF" } }, { - "name": "CoffeScript Parameter Function", + "name": "CoffeeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#bfc7d5" @@ -1523,14 +1523,14 @@ } }, { - "name": "handlebars enitity attribute names", + "name": "handlebars entity attribute names", "scope": "entity.other.attribute-name.handlebars", "settings": { "foreground": "#89DDFF" } }, { - "name": "handlebars enitity attribute values", + "name": "handlebars entity attribute values", "scope": "entity.other.attribute-value.handlebars variable.parameter.handlebars", "settings": { "foreground": "#7986E7" @@ -1558,7 +1558,7 @@ "keyword.operator.expression.in", "keyword.operator.type", "punctuation.section.embedded.js", - "punctuation.definintion.string", + "punctuation.definition.string", "punctuation" ], "settings": { diff --git a/crates/ai/src/prompts/base.rs b/crates/ai/src/prompts/base.rs index 75bad00154b001a356f35cb5d80e4ac4962fe0f9..5e624f23acb8e416676251ed728711756ea1ae6d 100644 --- a/crates/ai/src/prompts/base.rs +++ b/crates/ai/src/prompts/base.rs @@ -81,8 +81,8 @@ impl PromptChain { pub fn generate(&self, truncate: bool) -> anyhow::Result<(String, usize)> { // Argsort based on Prompt Priority - let seperator = "\n"; - let seperator_tokens = self.args.model.count_tokens(seperator)?; + let separator = "\n"; + let separator_tokens = self.args.model.count_tokens(separator)?; let mut sorted_indices = (0..self.templates.len()).collect::>(); sorted_indices.sort_by_key(|&i| Reverse(&self.templates[i].0)); @@ -104,7 +104,7 @@ impl PromptChain { prompts[idx] = template_prompt; if let Some(remaining_tokens) = tokens_outstanding { - let new_tokens = prompt_token_count + seperator_tokens; + let new_tokens = prompt_token_count + separator_tokens; tokens_outstanding = if remaining_tokens > new_tokens { Some(remaining_tokens - new_tokens) } else { @@ -117,9 +117,9 @@ impl PromptChain { prompts.retain(|x| x != ""); - let full_prompt = prompts.join(seperator); + let full_prompt = prompts.join(separator); let total_token_count = self.args.model.count_tokens(&full_prompt)?; - anyhow::Ok((prompts.join(seperator), total_token_count)) + anyhow::Ok((prompts.join(separator), total_token_count)) } } diff --git a/crates/ai/src/prompts/repository_context.rs b/crates/ai/src/prompts/repository_context.rs index 0d831c2cb2ee67dc1144d6f4c74493b8f27d84e7..89869c53a002c7451da892035b008d936a7c7f6d 100644 --- a/crates/ai/src/prompts/repository_context.rs +++ b/crates/ai/src/prompts/repository_context.rs @@ -68,7 +68,7 @@ impl PromptTemplate for RepositoryContext { let mut prompt = String::new(); let mut remaining_tokens = max_token_length.clone(); - let seperator_token_length = args.model.count_tokens("\n")?; + let separator_token_length = args.model.count_tokens("\n")?; for snippet in &args.snippets { let mut snippet_prompt = template.to_string(); let content = snippet.to_string(); @@ -79,9 +79,9 @@ impl PromptTemplate for RepositoryContext { if let Some(tokens_left) = remaining_tokens { if tokens_left >= token_count { writeln!(prompt, "{snippet_prompt}").unwrap(); - remaining_tokens = if tokens_left >= (token_count + seperator_token_length) + remaining_tokens = if tokens_left >= (token_count + separator_token_length) { - Some(tokens_left - token_count - seperator_token_length) + Some(tokens_left - token_count - separator_token_length) } else { Some(0) }; diff --git a/crates/ai/src/providers/open_ai/completion.rs b/crates/ai/src/providers/open_ai/completion.rs index c9a2abd0c8c3bc170ff39f16294f2fb976f00465..f99b7f95e346d275fade7628c0ea27375f84e7c9 100644 --- a/crates/ai/src/providers/open_ai/completion.rs +++ b/crates/ai/src/providers/open_ai/completion.rs @@ -273,7 +273,7 @@ impl CompletionProvider for OpenAICompletionProvider { ) -> BoxFuture<'static, Result>>> { // Currently the CompletionRequest for OpenAI, includes a 'model' parameter // This means that the model is determined by the CompletionRequest and not the CompletionProvider, - // which is currently model based, due to the langauge model. + // which is currently model based, due to the language model. // At some point in the future we should rectify this. let credential = self.credential.read().clone(); let request = stream_completion(credential, self.executor.clone(), prompt); diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index df3dc3754f66aff8d83a6fcd3b92edd38c7c4e45..ff12918e773efb3719402bc6212b7d69b51e73c6 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -2917,7 +2917,7 @@ impl InlineAssistant { let semantic_permissioned = self.semantic_permissioned(cx); if let Some(semantic_index) = SemanticIndex::global(cx) { cx.spawn(|_, mut cx| async move { - // This has to be updated to accomodate for semantic_permissions + // This has to be updated to accommodate for semantic_permissions if semantic_permissioned.await.unwrap_or(false) { semantic_index .update(&mut cx, |index, cx| index.index_project(project, cx))? diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 9eddb1f6187a80f4f88f8d13e9aff3f4c310941f..dc757e4d1a7cd48da4630dce15bcbc32c5186b6a 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -149,7 +149,7 @@ impl Database { .await?; // If the buffer epoch hasn't changed since the client lost - // connection, then the client's buffer can be syncronized with + // connection, then the client's buffer can be synchronized with // the server's buffer. if buffer.epoch as u64 != client_buffer.epoch { log::info!("can't rejoin buffer, epoch has changed"); @@ -962,7 +962,7 @@ fn version_from_storage(version: &Vec) -> Vec Option { match operation.variant? { proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index e80fe0fdca312bed54d98fce0a1ea69a8a4a6e86..7fbdf8ba7fc09a404f9824205c1d06d94c58f190 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -203,7 +203,7 @@ async fn test_core_channels( executor.run_until_parked(); // Observe that client B is now an admin of channel A, and that - // their admin priveleges extend to subchannels of channel A. + // their admin privileges extend to subchannels of channel A. assert_channel_invitations(client_b.channel_store(), cx_b, &[]); assert_channels( client_b.channel_store(), diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 0c3601b07531bf5c77459fd5530a31ba8ef68717..539e61ec964b545507e40d29abaf0fef88c862de 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -1201,7 +1201,7 @@ async fn test_on_input_format_from_host_to_guest( executor.run_until_parked(); // Receive an OnTypeFormatting request as the host's language server. - // Return some formattings from the host's language server. + // Return some formatting from the host's language server. fake_language_server.handle_request::( |params, _| async move { assert_eq!( @@ -1220,7 +1220,7 @@ async fn test_on_input_format_from_host_to_guest( }, ); - // Open the buffer on the guest and see that the formattings worked + // Open the buffer on the guest and see that the formatting worked let buffer_b = project_b .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await @@ -1339,7 +1339,7 @@ async fn test_on_input_format_from_guest_to_host( }); // Receive an OnTypeFormatting request as the host's language server. - // Return some formattings from the host's language server. + // Return some formatting from the host's language server. executor.start_waiting(); fake_language_server .handle_request::(|params, _| async move { @@ -1362,7 +1362,7 @@ async fn test_on_input_format_from_guest_to_host( .unwrap(); executor.finish_waiting(); - // Open the buffer on the host and see that the formattings worked + // Open the buffer on the host and see that the formatting worked let buffer_a = project_a .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await @@ -1836,7 +1836,7 @@ async fn test_inlay_hint_refresh_is_forwarded( assert_eq!( inlay_cache.version(), 1, - "Should update cache verison after first hints" + "Should update cache version after first hints" ); }); diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 11890bcbe6d3dace458827583c13d331c6670e7b..e92422d76d79e7b2706aed83b892031f9b4b3a96 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -111,7 +111,7 @@ impl ChannelModal { .detach(); } - fn set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext) { + fn set_channel_visibility(&mut self, selection: &Selection, cx: &mut ViewContext) { self.channel_store.update(cx, |channel_store, cx| { channel_store .set_channel_visibility( @@ -189,7 +189,7 @@ impl Render for ChannelModal { ui::Selection::Unselected }, ) - .on_click(cx.listener(Self::set_channel_visiblity)), + .on_click(cx.listener(Self::set_channel_visibility)), ) .child(Label::new("Public").size(LabelSize::Small)), ) diff --git a/crates/copilot_ui/src/sign_in.rs b/crates/copilot_ui/src/sign_in.rs index f78a82699dc3c70925accc3e70a3242e9aad5061..2bea2e016ce9eda8215fec825e3d10312af011a3 100644 --- a/crates/copilot_ui/src/sign_in.rs +++ b/crates/copilot_ui/src/sign_in.rs @@ -96,7 +96,7 @@ impl CopilotCodeVerification { .items_center() .child(Headline::new("Use Github Copilot in Zed.").size(HeadlineSize::Large)) .child( - Label::new("Using Copilot requres an active subscription on Github.") + Label::new("Using Copilot requires an active subscription on Github.") .color(Color::Muted), ) .child(Self::render_device_code(data, cx)) @@ -139,7 +139,7 @@ impl CopilotCodeVerification { "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.", ).color(Color::Warning)) .child( - Button::new("copilot-subscribe-button", "Subscibe on Github") + Button::new("copilot-subscribe-button", "Subscribe on Github") .full_width() .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)), ) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 30b0a73d37e093bcb456d4fa6cf8e0c2ff98d5ff..ca1f22d158fc87b1af4d92443898652e4d3cbb87 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8736,7 +8736,7 @@ impl Editor { ) { match event { multi_buffer::Event::Edited { - sigleton_buffer_edited, + singleton_buffer_edited, } => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); @@ -8746,7 +8746,7 @@ impl Editor { cx.emit(EditorEvent::BufferEdited); cx.emit(SearchEvent::MatchesInvalidated); - if *sigleton_buffer_edited { + if *singleton_buffer_edited { if let Some(project) = &self.project { let project = project.read(cx); let languages_affected = multibuffer diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 59c6b8605c1001440999e3f6909db300cf33e392..8fdc9f1a7de27ae67199d9cc11b937b1881bfa27 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -925,14 +925,14 @@ async fn fetch_and_update_hints( log::trace!("Fetched hints: {new_hints:?}"); let background_task_buffer_snapshot = buffer_snapshot.clone(); - let backround_fetch_range = fetch_range.clone(); + let background_fetch_range = fetch_range.clone(); let new_update = cx .background_executor() .spawn(async move { calculate_hint_updates( query.excerpt_id, invalidate, - backround_fetch_range, + background_fetch_range, new_hints, &background_task_buffer_snapshot, cached_excerpt_hints, @@ -1449,7 +1449,7 @@ pub mod tests { assert_eq!( editor.inlay_hint_cache().version, edits_made, - "Cache version should udpate once after the work task is done" + "Cache version should update once after the work task is done" ); }); } @@ -1599,7 +1599,7 @@ pub mod tests { assert_eq!( expected_hints, cached_hint_labels(editor), - "Markdown editor should have a separate verison, repeating Rust editor rules" + "Markdown editor should have a separate version, repeating Rust editor rules" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); @@ -2612,7 +2612,7 @@ pub mod tests { "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the version"); }); _ = editor.update(cx, |editor, cx| { @@ -2728,7 +2728,7 @@ pub mod tests { expected_hints, cached_hint_labels(editor), "After multibuffer edit, editor gets scolled back to the last selection; \ - all hints should be invalidated and requeried for all of its visible excerpts" + all hints should be invalidated and required for all of its visible excerpts" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index cf5c65105c9e9c473967f8adb9f7de04b6d8f567..ecb2a93577f20de437ea3762aa1d4a740848e294 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -29,7 +29,7 @@ pub trait GitRepository: Send { fn branch_name(&self) -> Option; /// Get the statuses of all of the files in the index that start with the given - /// path and have changes with resepect to the HEAD commit. This is fast because + /// path and have changes with respect to the HEAD commit. This is fast because /// the index stores hashes of trees, so that unchanged directories can be skipped. fn staged_statuses(&self, path_prefix: &Path) -> TreeMap; diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index fc60cb1ec6afcd1c79f5b561a436eac4635c47bc..585cbc80d197e99752cc6ea24ff71d8ed778097f 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -149,7 +149,7 @@ impl BackgroundExecutor { Task::Spawned(task) } - /// Used by the test harness to run an async test in a syncronous fashion. + /// Used by the test harness to run an async test in a synchronous fashion. #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn block_test(&self, future: impl Future) -> R { @@ -276,7 +276,7 @@ impl BackgroundExecutor { /// Returns a task that will complete after the given duration. /// Depending on other concurrent tasks the elapsed duration may be longer - /// than reqested. + /// than requested. pub fn timer(&self, duration: Duration) -> Task<()> { let (runnable, task) = async_task::spawn(async move {}, { let dispatcher = self.dispatcher.clone(); diff --git a/crates/gpui/src/keymap/matcher.rs b/crates/gpui/src/keymap/matcher.rs index 5410ddce06e9ca999aaee0911fd7a69d6a0e61c8..36c8035c8f27f10fccfe06b803a66a4fe9ed452c 100644 --- a/crates/gpui/src/keymap/matcher.rs +++ b/crates/gpui/src/keymap/matcher.rs @@ -445,7 +445,7 @@ mod tests { KeyMatch::Some(vec![Box::new(Dollar)]) ); - // handle Brazillian quote (quote key then space key) + // handle Brazilian quote (quote key then space key) assert_eq!( matcher.match_keystroke( &Keystroke::parse("space->\"").unwrap(), @@ -454,7 +454,7 @@ mod tests { KeyMatch::Some(vec![Box::new(Quote)]) ); - // handle ctrl+` on a brazillian keyboard + // handle ctrl+` on a brazilian keyboard assert_eq!( matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]), KeyMatch::Some(vec![Box::new(Backtick)]) diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index cadb9c92e3f4dc53d23671dfb630a8560bf55c7e..64a901789abb688c36c8dd2f2eef9c5fb16a34e8 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -19,7 +19,7 @@ impl Keystroke { // the ime_key or the key. On some non-US keyboards keys we use in our // bindings are behind option (for example `$` is typed `alt-ç` on a Czech keyboard), // and on some keyboards the IME handler converts a sequence of keys into a - // specific character (for example `"` is typed as `" space` on a brazillian keyboard). + // specific character (for example `"` is typed as `" space` on a brazilian keyboard). pub fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { let mut possibilities = SmallVec::new(); match self.ime_key.as_ref() { diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index 8f48b8ea94d8aa2193545267dd3d85a021a9f96c..3cc74a968399dcc0fffcf8c795137262e66df8de 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -10,7 +10,7 @@ mod open_type; mod platform; mod text_system; mod window; -mod window_appearence; +mod window_appearance; use crate::{px, size, GlobalPixels, Pixels, Size}; use cocoa::{ diff --git a/crates/gpui/src/platform/mac/window_appearence.rs b/crates/gpui/src/platform/mac/window_appearance.rs similarity index 100% rename from crates/gpui/src/platform/mac/window_appearence.rs rename to crates/gpui/src/platform/mac/window_appearance.rs diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 8fdb926b27ebc8e1ab6b4531a5902e0395f53f8b..095233280edefc0b11d85e3a4ee255f54c8da13d 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -42,7 +42,7 @@ pub struct Style { #[refineable] pub inset: Edges, - // Size properies + // Size properties /// Sets the initial size of the item #[refineable] pub size: Size, @@ -79,7 +79,7 @@ pub struct Style { #[refineable] pub gap: Size, - // Flexbox properies + // Flexbox properties /// Which direction does the main axis flow in? pub flex_direction: FlexDirection, /// Should elements wrap, or stay in a single line? @@ -502,7 +502,7 @@ impl Default for Style { max_size: Size::auto(), aspect_ratio: None, gap: Size::default(), - // Aligment + // Alignment align_items: None, align_self: None, align_content: None, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 0269ccfb6c8ef55df1696157302f6156e041f744..0e4bae3406416a75d8c24b07be20be7cbfa0a84b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2130,7 +2130,7 @@ impl<'a> WindowContext<'a> { .unwrap(); // Actual: Option <- View - // Requested: () <- AnyElemet + // Requested: () <- AnyElement let state = state_box .take() .expect("element state is already on the stack"); diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index 1187d96ca320abbc51b66b24b0639b1211b451dc..aef1785bb5a56c6acbc9d42d76286a0ed8a13133 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -53,7 +53,7 @@ pub fn style_helpers(input: TokenStream) -> TokenStream { /// variety of scenarios and interleavings just by changing the seed. /// /// #[gpui::test] also takes three different arguments: -/// - `#[gpui::test(interations=10)]` will run the test ten times with a different initial SEED. +/// - `#[gpui::test(iterations=10)]` will run the test ten times with a different initial SEED. /// - `#[gpui::test(retries=3)]` will run the test up to four times if it fails to try and make it pass. /// - `#[gpui::test(on_failure="crate::test::report_failure")]` will call the specified function after the /// tests fail so that you can write out more detail about the failure. diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 780483c5ca24fbb5ec43f9652b54f6c9ba5b0c30..6ad345d4e324d96dbe13333d0729db3cc6406671 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -275,7 +275,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { let version_before_format = format_diff.base_version.clone(); buffer.apply_diff(format_diff, cx); - // The outcome depends on the order of concurrent taks. + // The outcome depends on the order of concurrent tasks. // // If the edit occurred while searching for trailing whitespace ranges, // then the trailing whitespace region touched by the edit is left intact. diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 788c424373deca7c1490dd954fa005e0943d8a99..30cc0c07d96ecd218732ef4a24c5b2a60a5642a8 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -839,7 +839,7 @@ impl LanguageServer { futures::select! { response = rx.fuse() => { let elapsed = started.elapsed(); - log::trace!("Took {elapsed:?} to recieve response to {method:?} id {id}"); + log::trace!("Took {elapsed:?} to receive response to {method:?} id {id}"); response? } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index f3ecd2d25f2b3866b1f6c3f0ce68415fa7cd53ae..9f78480136e2e6eff71fb3016a2c9c99d4297b6f 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -72,7 +72,7 @@ pub enum Event { ids: Vec, }, Edited { - sigleton_buffer_edited: bool, + singleton_buffer_edited: bool, }, TransactionUndone { transaction_id: TransactionId, @@ -1112,7 +1112,7 @@ impl MultiBuffer { new: edit_start..edit_end, }]); cx.emit(Event::Edited { - sigleton_buffer_edited: false, + singleton_buffer_edited: false, }); cx.emit(Event::ExcerptsAdded { buffer, @@ -1138,7 +1138,7 @@ impl MultiBuffer { new: 0..0, }]); cx.emit(Event::Edited { - sigleton_buffer_edited: false, + singleton_buffer_edited: false, }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); @@ -1348,7 +1348,7 @@ impl MultiBuffer { self.subscriptions.publish_mut(edits); cx.emit(Event::Edited { - sigleton_buffer_edited: false, + singleton_buffer_edited: false, }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); @@ -1411,7 +1411,7 @@ impl MultiBuffer { ) { cx.emit(match event { language::Event::Edited => Event::Edited { - sigleton_buffer_edited: true, + singleton_buffer_edited: true, }, language::Event::DirtyChanged => Event::DirtyChanged, language::Event::Saved => Event::Saved, @@ -4280,13 +4280,13 @@ mod tests { events.read().as_slice(), &[ Event::Edited { - sigleton_buffer_edited: false + singleton_buffer_edited: false }, Event::Edited { - sigleton_buffer_edited: false + singleton_buffer_edited: false }, Event::Edited { - sigleton_buffer_edited: false + singleton_buffer_edited: false } ] ); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 52836f4c0030e005eb19d396c77bfb49fc0ea604..2c2bed87173e2d3e7fff124815e4f89f852a103b 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2253,7 +2253,7 @@ impl LspCommand for InlayHints { language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning // `const foo: boolean` into `const foo : boolean` which looks odd. - // `rust-analyzer` does not have the padding for this case, and we have to accomodate both. + // `rust-analyzer` does not have the padding for this case, and we have to accommodate both. // // We could trim the whole string, but being pessimistic on par with the situation above, // there might be a hint with multiple whitespaces at the end(s) which we need to display properly. diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5f37bbfce6483e359866a0dadb1d63b2e32b4651..c5dc88d4479ef1627a7da56344c16695077756b3 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5578,7 +5578,7 @@ impl Project { // 3. We run a scan over all the candidate buffers on multiple background threads. // We cannot assume that there will even be a match - while at least one match // is guaranteed for files obtained from FS, the buffers we got from memory (unsaved files/unnamed buffers) might not have a match at all. - // There is also an auxilliary background thread responsible for result gathering. + // There is also an auxiliary background thread responsible for result gathering. // This is where the sorted list of buffers comes into play to maintain sorted order; Whenever this background thread receives a notification (buffer has/doesn't have matches), // it keeps it around. It reports matches in sorted order, though it accepts them in unsorted order as well. // As soon as the match info on next position in sorted order becomes available, it reports it (if it's a match) or skips to the next @@ -8550,7 +8550,7 @@ fn glob_literal_prefix<'a>(glob: &'a str) -> &'a str { break; } else { if i > 0 { - // Acount for separator prior to this part + // Account for separator prior to this part literal_end += path::MAIN_SEPARATOR.len_utf8(); } literal_end += part.len(); diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 925109ac964044a2e93a4a1a7ba23b493a2434ed..9ec07bc088c49ef541b2af1b0c7ee1164f9a9eec 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -9,7 +9,7 @@ use std::sync::Arc; pub struct ProjectSettings { /// Configuration for language servers. /// - /// The following settings can be overriden for specific language servers: + /// The following settings can be overridden for specific language servers: /// - initialization_options /// To override settings for a language, add an entry for that language server's /// name to the lsp value. diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index 82aebe7913133d2aa7f673f21387f2c7a8ad3ca3..0ddcfc9285eb31b7f5be50d57d6ef8dd14cebb42 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -44,7 +44,7 @@ impl FileAssociations { pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { let this = cx.has_global::().then(|| cx.global::())?; - // FIXME: Associate a type with the languages and have the file's langauge + // FIXME: Associate a type with the languages and have the file's language // override these associations maybe!({ let suffix = path.icon_suffix()?; diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 6208635e22d969bfa9219d7eb4d1466a35a0999d..1d8ddefcbcc674ff22f85aeda5c9f3fc67dc4a85 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -32,7 +32,7 @@ impl RecentProjects { fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext) -> Self { let picker = cx.new_view(|cx| Picker::new(delegate, cx)); let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent)); - // We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap + // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap // out workspace locations once the future runs to completion. cx.spawn(|this, mut cx| async move { let workspaces = WORKSPACE_DB diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs index 99418206462a0dc7bc3babd2f9bda534a69a0f39..bc906daece556106978f9788ff1502f004812180 100644 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ b/crates/refineable/derive_refineable/src/derive_refineable.rs @@ -141,7 +141,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { }) .collect(); - let refinement_refine_assigments: Vec = fields + let refinement_refine_assignments: Vec = fields .iter() .map(|field| { let name = &field.ident; @@ -161,7 +161,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { }) .collect(); - let refinement_refined_assigments: Vec = fields + let refinement_refined_assignments: Vec = fields .iter() .map(|field| { let name = &field.ident; @@ -181,7 +181,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { }) .collect(); - let from_refinement_assigments: Vec = fields + let from_refinement_assignments: Vec = fields .iter() .map(|field| { let name = &field.ident; @@ -272,11 +272,11 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { type Refinement = #refinement_ident #ty_generics; fn refine(&mut self, refinement: &Self::Refinement) { - #( #refinement_refine_assigments )* + #( #refinement_refine_assignments )* } fn refined(mut self, refinement: Self::Refinement) -> Self { - #( #refinement_refined_assigments )* + #( #refinement_refined_assignments )* self } } @@ -286,7 +286,7 @@ pub fn derive_refineable(input: TokenStream) -> TokenStream { { fn from(value: #refinement_ident #ty_generics) -> Self { Self { - #( #from_refinement_assigments )* + #( #from_refinement_assignments )* } } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 49eb24ce9ee9e267dc921b6ddb8eb10e92c83c97..55fe39310cd8bc2084a2644f8eaab49a83e891cb 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -2579,7 +2579,7 @@ pub mod tests { ); assert!( search_view_2.query_editor.focus_handle(cx).is_focused(cx), - "Focus should be moved into query editor fo the new window" + "Focus should be moved into query editor of the new window" ); }); }).unwrap(); diff --git a/crates/semantic_index/README.md b/crates/semantic_index/README.md index 85f83af121ed96a51ac84165c19cda3cd8aff7d4..75ccb41b84468bef319b6bc1957fd236c125ba95 100644 --- a/crates/semantic_index/README.md +++ b/crates/semantic_index/README.md @@ -10,7 +10,7 @@ nDCG@k: - "The relevance of result is represented by a score (also known as a 'grade') that is assigned to the search query. The scores of these results are then discounted based on their position in the search results -- did they get recommended first or last?" MRR@k: -- "Mean reciprocal rank quantifies the rank of the first relevant item found in teh recommendation list." +- "Mean reciprocal rank quantifies the rank of the first relevant item found in the recommendation list." MAP@k: - "Mean average precision averages the precision@k metric at each relevant item position in the recommendation list. diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index 427ac158c1b9e84ed4c64ca6700e08cc31269b7d..9f2db711ae0e97d5f7f21af7afd12f09de147a18 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -76,7 +76,7 @@ pub struct CodeContextRetriever { // Every match has an item, this represents the fundamental treesitter symbol and anchors the search // Every match has one or more 'name' captures. These indicate the display range of the item for deduplication. -// If there are preceeding comments, we track this with a context capture +// If there are preceding comments, we track this with a context capture // If there is a piece that should be collapsed in hierarchical queries, we capture it with a collapse capture // If there is a piece that should be kept inside a collapsed node, we capture it with a keep capture #[derive(Debug, Clone)] diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index e340b44a58377b8a9bda52786dea660637ce54c1..86eb6b84041fced41d1a0c433ebf62d10634c58f 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -110,7 +110,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) { cx, ); - // Test Include Files Functonality + // Test Include Files Functionality let include_files = vec![PathMatcher::new("*.rs").unwrap()]; let exclude_files = vec![PathMatcher::new("*.rs").unwrap()]; let rust_only_search_results = semantic_index @@ -576,7 +576,7 @@ async fn test_code_context_retrieval_lua() { setmetatable(classdef, { __index = baseclass }) -- All class instances have a reference to the class object. classdef.class = classdef - --- Recursivly allocates the inheritance tree of the instance. + --- Recursively allocates the inheritance tree of the instance. -- @param mastertable The 'root' of the inheritance tree. -- @return Returns the instance with the allocated inheritance tree. function classdef.alloc(mastertable) @@ -607,7 +607,7 @@ async fn test_code_context_retrieval_lua() { setmetatable(classdef, { __index = baseclass }) -- All class instances have a reference to the class object. classdef.class = classdef - --- Recursivly allocates the inheritance tree of the instance. + --- Recursively allocates the inheritance tree of the instance. -- @param mastertable The 'root' of the inheritance tree. -- @return Returns the instance with the allocated inheritance tree. function classdef.alloc(mastertable) @@ -617,7 +617,7 @@ async fn test_code_context_retrieval_lua() { end"#.unindent(), 114), (r#" - --- Recursivly allocates the inheritance tree of the instance. + --- Recursively allocates the inheritance tree of the instance. -- @param mastertable The 'root' of the inheritance tree. -- @return Returns the instance with the allocated inheritance tree. function classdef.alloc(mastertable) diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index c0d125d4df9f654364088f6af31c40c9f3192950..f59b9dd40eddbce68412484fded2a729af0fefee 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -191,7 +191,7 @@ mod test { fn migrations_dont_rerun() { let connection = Connection::open_memory(Some("migrations_dont_rerun")); - // Create migration which clears a tabl + // Create migration which clears a table // Manually create the table for that migration with a row connection diff --git a/crates/storybook/src/stories/text.rs b/crates/storybook/src/stories/text.rs index 065b5bf795ba89fa1747c40fc1387ff00d4cb9c0..b7445ef95aac857a258c903a64ccde0c25497461 100644 --- a/crates/storybook/src/stories/text.rs +++ b/crates/storybook/src/stories/text.rs @@ -65,7 +65,7 @@ impl Render for TextStory { )) ) .usage(indoc! {r##" - // NOTE: When rendering text in a horizonal flex container, + // NOTE: When rendering text in a horizontal flex container, // Taffy will not pass width constraints down from the parent. // To fix this, render text in a parent with overflow: hidden @@ -149,7 +149,7 @@ impl Render for TextStory { // "Meanwhile, the lazy dog decided it was time for a change. ", // "He started daily workout routines, ate healthier and became the fastest dog in town.", // )))) -// // NOTE: When rendering text in a horizonal flex container, +// // NOTE: When rendering text in a horizontal flex container, // // Taffy will not pass width constraints down from the parent. // // To fix this, render text in a parent with overflow: hidden // .child(div().h_5()) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index db4b21627f13a3e050888ca1cf18a06488484b1a..2f5e3ccf78bb9dfd557ee74f06636d666970850b 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -674,9 +674,9 @@ impl Render for TerminalView { self.can_navigate_to_selected_word, )), ) - .children(self.context_menu.as_ref().map(|(menu, positon, _)| { + .children(self.context_menu.as_ref().map(|(menu, position, _)| { overlay() - .position(*positon) + .position(*position) .anchor(gpui::AnchorCorner::TopLeft) .child(menu.clone()) })) diff --git a/crates/theme/src/styles/syntax.rs b/crates/theme/src/styles/syntax.rs index 0f35bf60a73aa634bf0953e19f4beea1354131c3..d6189f73e3e10da39918932559503787a129af64 100644 --- a/crates/theme/src/styles/syntax.rs +++ b/crates/theme/src/styles/syntax.rs @@ -127,7 +127,7 @@ impl SyntaxTheme { } } - // TOOD: Get this working with `#[cfg(test)]`. Why isn't it? + // TODO: Get this working with `#[cfg(test)]`. Why isn't it? pub fn new_test(colors: impl IntoIterator) -> Self { SyntaxTheme { highlights: colors diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs index ff20d36a5df6ad3ffebbe0dc4f58a85ec1383707..0861b7efd8433729707a3c7d4d6fe8541366226a 100644 --- a/crates/theme_importer/src/main.rs +++ b/crates/theme_importer/src/main.rs @@ -188,7 +188,7 @@ fn main() -> Result<()> { let zed1_themes_path = PathBuf::from_str("assets/themes")?; - let zed1_theme_familes = [ + let zed1_theme_families = [ "Andromeda", "Atelier", "Ayu", @@ -207,7 +207,7 @@ fn main() -> Result<()> { ); let mut zed1_themes_by_family: IndexMap> = IndexMap::from_iter( - zed1_theme_familes + zed1_theme_families .into_iter() .map(|family| (family.to_string(), Vec::new())), ); diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 018d31dafdb4491bc88733117c8e42f66e789aa3..c2910acfc04bee67630c3d55b2ce2394559379f6 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -111,7 +111,7 @@ pub enum ButtonStyle { #[default] Subtle, - /// Used for buttons that only change forground color on hover and active states. + /// Used for buttons that only change foreground color on hover and active states. /// /// TODO: Better docs for this. Transparent, diff --git a/crates/ui/src/components/popover.rs b/crates/ui/src/components/popover.rs index 2e0c5bfec87b84f820bf3bdb512053460e90360f..ad72a1d9b6efd45cf16270ac0cb552b46732957a 100644 --- a/crates/ui/src/components/popover.rs +++ b/crates/ui/src/components/popover.rs @@ -12,7 +12,7 @@ use smallvec::SmallVec; /// user's mouse.) /// /// Example: A "new" menu with options like "new file", "new folder", etc, -/// Linear's "Display" menu, a profile menu that appers when you click your avatar. +/// Linear's "Display" menu, a profile menu that appears when you click your avatar. /// /// Related elements: /// diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index ec1848ca6093606aea9abcc926f242b20e0e727c..0aa3786a279242c8a3a301b497a60ab0c2c537ec 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -85,7 +85,7 @@ impl LayerIndex { } } -/// An appropriate z-index for the given layer based on its intended useage. +/// An appropriate z-index for the given layer based on its intended usage. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ElementIndex { Effect, diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 67a12a852bdaeeae08f5fbcdc41ab11a91572605..44564ce878eae530cf23f48ea02ad44d93cd4892 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -342,7 +342,7 @@ impl PickerDelegate for BranchListDelegate { } let status = repo.change_branch(¤t_pick); if status.is_err() { - this.delegate.display_error_toast(format!("Failed to chec branch '{current_pick}', check for conflicts or unstashed files"), cx); + this.delegate.display_error_toast(format!("Failed to check branch '{current_pick}', check for conflicts or unstashed files"), cx); status?; } this.cancel(&Default::default(), cx); diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index a643c126ef4edbd00442578059b9db55cb3e5774..e1570f9d6415a8878352c0b2c9c314313d8f6c51 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -152,7 +152,7 @@ pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) { let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1); - // if we came from insert mode we're just doing repititions 2 onwards. + // if we came from insert mode we're just doing repetitions 2 onwards. if from_insert_mode { count -= 1; new_actions[0] = actions[0].clone(); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 7b5f4d3e59a3362c807922ae12ffaba98fdfd8eb..f85e3d9ba92415040114fbcfd61c55c5066dbf79 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -278,7 +278,7 @@ fn parse_replace_all(query: &str) -> Replacement { return Replacement::default(); } - let Some(delimeter) = chars.next() else { + let Some(delimiter) = chars.next() else { return Replacement::default(); }; @@ -301,13 +301,13 @@ fn parse_replace_all(query: &str) -> Replacement { buffer.push('$') // unescape escaped parens } else if phase == 0 && c == '(' || c == ')' { - } else if c != delimeter { + } else if c != delimiter { buffer.push('\\') } buffer.push(c) } else if c == '\\' { escaped = true; - } else if c == delimeter { + } else if c == delimiter { if phase == 0 { buffer = &mut replacement; phase = 1; diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 363f6d43e38ab2a99cab89173e62ddc81bfe02f2..a2daf7499d887eee0d7cd6de363f305f7740387c 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -359,7 +359,7 @@ impl NeovimConnection { // to add one to the end in visual mode. match mode { Some(Mode::VisualBlock) if selection_row != cursor_row => { - // in zed we fake a block selecrtion by using multiple cursors (one per line) + // in zed we fake a block selection by using multiple cursors (one per line) // this code emulates that. // to deal with casees where the selection is not perfectly rectangular we extract // the content of the selection via the "a register to get the shape correctly. diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1fd11167c6843206ddcef2b4ccee96a0f9886dae..0f29101d974f26e9c714ac47c1ade3331950b444 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -201,14 +201,14 @@ pub fn visual_block_motion( let mut row = tail.row(); loop { - let layed_out_line = map.layout_row(row, &text_layout_details); + let laid_out_line = map.layout_row(row, &text_layout_details); let start = DisplayPoint::new( row, - layed_out_line.closest_index_for_x(positions.start) as u32, + laid_out_line.closest_index_for_x(positions.start) as u32, ); let mut end = DisplayPoint::new( row, - layed_out_line.closest_index_for_x(positions.end) as u32, + laid_out_line.closest_index_for_x(positions.end) as u32, ); if end <= start { if start.column() == map.line_len(start.row()) { @@ -218,7 +218,7 @@ pub fn visual_block_motion( } } - if positions.start <= layed_out_line.width { + if positions.start <= laid_out_line.width { let selection = Selection { id: s.new_selection_id(), start: start.to_point(map), diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/welcome/src/base_keymap_setting.rs index e05a16c350d4de2a7f1c0ddf56b7b581ef7d6b17..54af63007af38b7796b92a9280aa951056fc6f52 100644 --- a/crates/welcome/src/base_keymap_setting.rs +++ b/crates/welcome/src/base_keymap_setting.rs @@ -4,7 +4,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -/// Base key bindings scheme. Base keymaps can be overriden with user keymaps. +/// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// /// Default: VSCode #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a1f3e6992aef51291fc66b9f4092e3dbd24c7be6..3e88469aa8593967e3ffea1cfd3ba0392f7e11f7 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1485,7 +1485,7 @@ impl Pane { .child( div() .min_w_6() - // HACK: This empty child is currently necessary to force the drop traget to appear + // HACK: This empty child is currently necessary to force the drop target to appear // despite us setting a min width above. .child("") .h_full() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1d06db5de3b459b289d3b0392c3fb91ac594a760..e8589849f14dbe9148ff467480f5d4a5583d1ed7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2617,7 +2617,7 @@ impl Workspace { // If the item belongs to a particular project, then it should // only be included if this project is shared, and the follower - // is in thie project. + // is in the project. // // Some items, like channel notes, do not belong to a particular // project, so they should be included regardless of whether the diff --git a/docs/old/building-zed.md b/docs/old/building-zed.md index ec4538cf85f47d99ee11b24bb876b602eba8249e..79db4a36c9ec65fa54807fa2234ee8190729f067 100644 --- a/docs/old/building-zed.md +++ b/docs/old/building-zed.md @@ -36,7 +36,7 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos - Keep the token in the browser tab/editor for the next two steps -1. (Optional but reccomended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` +1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` 1. Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies: ``` cd .. diff --git a/docs/old/zed/syntax-highlighting.md b/docs/old/zed/syntax-highlighting.md index d4331ee367934453b19c6cd30af57924409b34d7..846bf968764f7dcf16210c7f740ee8a499cc53c4 100644 --- a/docs/old/zed/syntax-highlighting.md +++ b/docs/old/zed/syntax-highlighting.md @@ -4,7 +4,7 @@ This doc is a work in progress! ## Defining syntax highlighting rules -We use tree-sitter queries to match certian properties to highlight. +We use tree-sitter queries to match certain properties to highlight. ### Simple Example: diff --git a/docs/src/configuring_zed.md b/docs/src/configuring_zed.md index 9b9205f70ca7cac1cb587a2800de6af3f7bed956..46f0d35becb63d6b86ace12cc37bea2b8aaee297 100644 --- a/docs/src/configuring_zed.md +++ b/docs/src/configuring_zed.md @@ -4,7 +4,7 @@ Folder-specific settings are used to override Zed's global settings for files within a specific directory in the project panel. To get started, create a `.zed` subdirectory and add a `settings.json` within it. It should be noted that folder-specific settings don't need to live only a project's root, but can be defined at multiple levels in the project hierarchy. In setups like this, Zed will find the configuration nearest to the file you are working in and apply those settings to it. In most cases, this level of flexibility won't be needed and a single configuration for all files in a project is all that is required; the `Zed > Settings > Open Local Settings` menu action is built for this case. Running this action will look for a `.zed/settings.json` file at the root of the first top-level directory in your project panel. If it does not exist, it will create it. -The following global settings can be overriden with a folder-specific configuration: +The following global settings can be overridden with a folder-specific configuration: - `copilot` - `enable_language_server` diff --git a/docs/src/developing_zed__adding_languages.md b/docs/src/developing_zed__adding_languages.md index 2917b08422c1e2153a38f5c857a9318a9228c031..7fce7e85446ec355835bf137f41669bfea1115db 100644 --- a/docs/src/developing_zed__adding_languages.md +++ b/docs/src/developing_zed__adding_languages.md @@ -8,7 +8,7 @@ Zed uses the [Language Server Protocol](https://microsoft.github.io/language-ser ### Defining syntax highlighting rules -We use tree-sitter queries to match certian properties to highlight. +We use tree-sitter queries to match certain properties to highlight. #### Simple Example: diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index 7606e369d05cf02226ea5b783f4ff8369a661be3..7d1b40f924e6637960a55eee7dbb99f0dbedc038 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -39,7 +39,7 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos - Keep the token in the browser tab/editor for the next two steps -1. (Optional but reccomended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` +1. (Optional but recommended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` 1. Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: ``` cd .. From 9a7d2e3fe4065a4611eb3311d6721edf599d2683 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 14:44:10 -0800 Subject: [PATCH 55/96] fmt --- crates/vim/src/visual.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 0f29101d974f26e9c714ac47c1ade3331950b444..ad6486ab0c5157f5b860b793e4809e7cd2e8f1a4 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -206,10 +206,8 @@ pub fn visual_block_motion( row, laid_out_line.closest_index_for_x(positions.start) as u32, ); - let mut end = DisplayPoint::new( - row, - laid_out_line.closest_index_for_x(positions.end) as u32, - ); + let mut end = + DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32); if end <= start { if start.column() == map.line_len(start.row()) { end = start; From 285f4d1be9ed847d94d5f1126c37284e8b0e1c84 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 14:57:13 -0800 Subject: [PATCH 56/96] Fix busted test --- crates/semantic_index/src/semantic_index_tests.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 86eb6b84041fced41d1a0c433ebf62d10634c58f..9da92a15a8acf6dd6ae8bf06d1846fc5073ffee1 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -626,7 +626,7 @@ async fn test_code_context_retrieval_lua() { -- Any functions this instance does not know of will 'look up' to the superclass definition. setmetatable(instance, { __index = classdef, __newindex = mastertable }) return instance - end"#.unindent(), 809), + end"#.unindent(), 810), ] ); } From e6ca92ffa4e5cb88c438d048d7f729bc1df327f8 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 14:58:58 -0800 Subject: [PATCH 57/96] Fix a few more typos --- CONTRIBUTING.md | 2 +- crates/collab/src/rpc.rs | 2 +- crates/gpui/src/app.rs | 4 ++-- crates/gpui/src/elements/list.rs | 2 +- crates/gpui/src/view.rs | 2 +- crates/vim/src/vim.rs | 2 +- docs/old/release-process.md | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c45475e4ca8ccbfbd1b22b9bc6270ca34c28cd3..a85be833214d83d9cd6dcb24d6a41d3974a56595 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,4 +54,4 @@ We're happy to pair with you to help you learn the codebase and get your contrib Reviewing code in a pull request, after the fact, is hard and tedious - the team generally likes to build trust and review code through pair programming. We'd prefer have conversations about the code, through Zed, while it is being written, so decisions can be made in real-time and less time is spent on fixing things after the fact. Ideally, GitHub is only used to merge code that has already been discussed and reviewed in Zed. -Remeber that smaller, incremental PRs are easier to review and merge than large PRs. +Remember that smaller, incremental PRs are easier to review and merge than large PRs. diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9406b4938a8f2795a89cd8f10238964710eb61f3..f9218e5634cdd0d3c104d890fc4d7f3396bb1b06 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -3116,7 +3116,7 @@ async fn leave_channel_chat(request: proto::LeaveChannelChat, session: Session) Ok(()) } -/// Retrive the chat history for a channel +/// Retrieve the chat history for a channel async fn get_channel_messages( request: proto::GetChannelMessages, response: Response, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ab9b4d9f86417c13127de5a8758297c8d4ce0028..8f7345ae16ced7e84cb145de81635b8c805e3fe6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -967,8 +967,8 @@ impl AppContext { } /// Register a callback to be invoked when a keystroke is received by the application - /// in any window. Note that this fires after all other action and event mechansims have resolved - /// and that this API will not be invoked if the event's propogation is stopped. + /// in any window. Note that this fires after all other action and event mechanisms have resolved + /// and that this API will not be invoked if the event's propagation is stopped. pub fn observe_keystrokes( &mut self, f: impl FnMut(&KeystrokeEvent, &mut WindowContext) + 'static, diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index c0874a8dd4116275846edc24c1ebcfcfd9d752b6..2921d90a10088d8a4f03018b6f29f8d2416e110a 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -615,7 +615,7 @@ mod test { // Reset state.reset(5); - // And then recieve a scroll event _before_ the next paint + // And then receive a scroll event _before_ the next paint cx.simulate_event(ScrollWheelEvent { position: point(px(1.), px(1.)), delta: ScrollDelta::Pixels(point(px(0.), px(-500.))), diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index 3701bbbd69419daff04e83fdc85413099d34f42f..6426ac4c32f05e0765f2b5ca3cdb81ee603edc6e 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -200,7 +200,7 @@ impl PartialEq for WeakView { impl Eq for WeakView {} -/// A dynically-typed handle to a view, which can be downcast to a [View] for a specific type. +/// A dynamically-typed handle to a view, which can be downcast to a [View] for a specific type. #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index e03efb0a64b4b55e92ed8d92d8837351ac573e0d..0cb038807bf44be70dbd296276f669a0c69bff63 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -332,7 +332,7 @@ impl Vim { } } - /// Explicitly record one action (equiavlent to start_recording and stop_recording) + /// Explicitly record one action (equivalents to start_recording and stop_recording) pub fn record_current_action(&mut self, cx: &mut WindowContext) { self.start_recording(cx); self.stop_recording(); diff --git a/docs/old/release-process.md b/docs/old/release-process.md index 6162304a7b0a74fe3bfad2cc110d9fbc6a820c51..fc237d959069188d26ea85c7417a3a079377997b 100644 --- a/docs/old/release-process.md +++ b/docs/old/release-process.md @@ -90,7 +90,7 @@ This means that when releasing a new version of Zed that has changes to the RPC 1. If needing a migration: - First check that the migration is valid. The database serves both preview and stable simultaneously, so new columns need to have defaults and old tables or columns can't be dropped. - Then use `script/deploy-migration` (production, staging, preview, nightly). ex: `script/deploy-migration preview 0.19.0` - - If there is an 'Error: container is waiting to start', you can review logs manually with: `kubectl --namespace logs ` to make sure the mgiration ran successfully. + - If there is an 'Error: container is waiting to start', you can review logs manually with: `kubectl --namespace logs ` to make sure the mgiration ran successfully. 1. Once that CI job completes, you will be able to run the following command to deploy that docker image. The script takes two arguments: an environment (`production`, `preview`, or `staging`), and a version number (e.g. `0.10.1`): ``` From e42a9ac2f103bf224ed74e5844b20845b47108cf Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 15:22:37 -0800 Subject: [PATCH 58/96] Add typos configuration for zed and add a few more typo fixes --- crates/collab/src/tests/following_tests.rs | 7 ++++++- crates/search/src/history.rs | 2 +- typos.toml | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 typos.toml diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index af184d7d02a3458deddb7bfdbf1f77be48d70790..b3af077e9d177ba3a3580fe4ea2a8c92523aeac1 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1735,6 +1735,11 @@ async fn test_following_into_excluded_file( vec![18..17] ); + editor_for_excluded_a.update(cx_a, |editor, cx| { + editor.select_right(&Default::default(), cx); + }); + executor.run_until_parked(); + // Changes from B to the excluded file are replicated in A's editor editor_for_excluded_b.update(cx_b, |editor, cx| { editor.handle_input("\nCo-Authored-By: B ", cx); @@ -1743,7 +1748,7 @@ async fn test_following_into_excluded_file( editor_for_excluded_a.update(cx_a, |editor, cx| { assert_eq!( editor.text(cx), - "new commit messag\nCo-Authored-By: B " + "new commit message\nCo-Authored-By: B " ); }); } diff --git a/crates/search/src/history.rs b/crates/search/src/history.rs index 6b06c60293d4389693b9d3692a2649856076081f..5571313acb280c143b70e05fb9c5f8aa066bf56e 100644 --- a/crates/search/src/history.rs +++ b/crates/search/src/history.rs @@ -85,7 +85,7 @@ mod tests { assert_eq!( search_history.current(), None, - "No current selection should be set fo the default search history" + "No current selection should be set for the default search history" ); search_history.add("rust".to_string()); diff --git a/typos.toml b/typos.toml new file mode 100644 index 0000000000000000000000000000000000000000..47eb2a0d9c493d4333d808310c99c2632f55e913 --- /dev/null +++ b/typos.toml @@ -0,0 +1,19 @@ +[files] +ignore-files = true +extend-exclude = [ + # Vim makes heavy use of partial typing tables + "crates/vim/*", + # glsl isn't recognized by this tool + "crates/zed/src/languages/glsl/*", + # File suffixes aren't typos + "assets/icons/file_icons/file_types.json", + # :/ + "crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql", + # Editor and file finder rely on partial typing and custom in-string syntax + "crates/file_finder/src/file_finder.rs", + "crates/editor/src/editor_tests.rs", +] + +[default] +extend-ignore-re = ["ba"] +check-filename = true \ No newline at end of file From e3e3ef528e71f032a3d90535c283c601a33b1d14 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 15:28:25 -0800 Subject: [PATCH 59/96] Add typos ci --- .github/actions/check_style/action.yml | 3 +++ .github/workflows/ci.yml | 3 +-- crates/gpui/src/gpui.rs | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/actions/check_style/action.yml b/.github/actions/check_style/action.yml index 25020e4e4c5399026c1ab32622903a3779ba86b2..290496d7e7866490286fafeb31d867b73c20ab80 100644 --- a/.github/actions/check_style/action.yml +++ b/.github/actions/check_style/action.yml @@ -21,3 +21,6 @@ runs: run: | export SQUAWK_GITHUB_TOKEN=${{ github.token }} . ./script/squawk + + - name: Run spelling check + uses: crate-ci/typos@master diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 476f263997d5caa7fb0f3cb576397b618347e82f..7b9ca8de8e015de0161a1be1c8fddb1f3fa9d825 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ env: jobs: style: - name: Check formatting and Clippy lints + name: Check formatting, Clippy lints, and spelling runs-on: - self-hosted - test @@ -40,7 +40,6 @@ jobs: - name: Run style checks uses: ./.github/actions/check_style - tests: name: Run tests runs-on: diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 6f5e30149d9691b3c364d62ab2e3ce6ec7da1b4c..1c7e7432889b2a20ba2c3bfaf850581c8c323a5c 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -88,6 +88,7 @@ use std::{ }; use taffy::TaffyLayoutEngine; +/// Here's a spelling mistake: visibile pub trait Context { type Result; From 6cbc49e5f016f25223d1567d84f7f8c99bfada8f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:48:37 +0100 Subject: [PATCH 60/96] Editor docs (#4097) Release Notes: - N/A --------- Co-authored-by: Kirill --- crates/assistant/src/assistant_panel.rs | 17 +- crates/collab/src/tests/editor_tests.rs | 12 +- crates/collab/src/tests/following_tests.rs | 4 +- crates/copilot_ui/src/copilot_button.rs | 2 +- crates/diagnostics/src/diagnostics.rs | 2 +- crates/diagnostics/src/items.rs | 2 +- crates/editor/src/actions.rs | 218 ++++++++++++++ crates/editor/src/display_map.rs | 14 +- crates/editor/src/display_map/block_map.rs | 2 +- crates/editor/src/display_map/fold_map.rs | 2 +- crates/editor/src/display_map/inlay_map.rs | 6 +- crates/editor/src/display_map/wrap_map.rs | 2 +- crates/editor/src/editor.rs | 273 +++--------------- crates/editor/src/element.rs | 2 +- crates/editor/src/inlay_hint_cache.rs | 59 +++- crates/editor/src/link_go_to_definition.rs | 2 +- crates/editor/src/movement.rs | 72 ++++- crates/editor/src/scroll.rs | 13 +- crates/editor/src/scroll/autoscroll.rs | 2 +- crates/editor/src/selections_collection.rs | 14 +- crates/file_finder/src/file_finder.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/journal/src/journal.rs | 2 +- crates/language_tools/src/lsp_log.rs | 2 +- crates/language_tools/src/syntax_tree_view.rs | 2 +- crates/outline/src/outline.rs | 4 +- crates/project_panel/src/project_panel.rs | 2 +- crates/project_symbols/src/project_symbols.rs | 2 +- .../quick_action_bar/src/quick_action_bar.rs | 4 +- crates/search/src/buffer_search.rs | 4 +- crates/search/src/project_search.rs | 8 +- .../src/stories/auto_height_editor.rs | 6 +- crates/terminal_view/src/terminal_view.rs | 4 +- crates/vim/src/command.rs | 32 +- crates/vim/src/insert.rs | 2 +- crates/vim/src/motion.rs | 5 +- crates/vim/src/normal.rs | 2 +- crates/vim/src/normal/case.rs | 2 +- crates/vim/src/normal/change.rs | 7 +- crates/vim/src/normal/delete.rs | 2 +- crates/vim/src/normal/increment.rs | 2 +- crates/vim/src/normal/paste.rs | 3 +- crates/vim/src/normal/repeat.rs | 2 +- crates/vim/src/normal/scroll.rs | 2 +- crates/vim/src/object.rs | 5 +- crates/vim/src/visual.rs | 2 +- crates/zed/src/app_menus.rs | 49 ++-- crates/zed/src/open_listener.rs | 2 +- crates/zed/src/zed.rs | 2 +- 49 files changed, 508 insertions(+), 377 deletions(-) create mode 100644 crates/editor/src/actions.rs diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 241b9af923e44da0139bf07d3a57631c6c7e353b..20d5efaaa868b07929916bf8c6dddc12f5a22520 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -19,12 +19,13 @@ use chrono::{DateTime, Local}; use client::telemetry::AssistantKind; use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ + actions::{MoveDown, MoveUp}, display_map::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, - scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MoveDown, MoveUp, MultiBufferSnapshot, - ToOffset, ToPoint, + scroll::{Autoscroll, AutoscrollStrategy}, + Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, ToOffset, + ToPoint, }; use fs::Fs; use futures::StreamExt; @@ -479,7 +480,7 @@ impl AssistantPanel { fn cancel_last_inline_assist( workspace: &mut Workspace, - _: &editor::Cancel, + _: &editor::actions::Cancel, cx: &mut ViewContext, ) { if let Some(panel) = workspace.panel::(cx) { @@ -891,7 +892,7 @@ impl AssistantPanel { } } - fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + fn handle_editor_cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { if !search_bar.read(cx).is_dismissed() { search_bar.update(cx, |search_bar, cx| { @@ -2158,7 +2159,7 @@ impl ConversationEditor { } } - fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { if !self .conversation .update(cx, |conversation, _| conversation.cancel_last_assist()) @@ -2417,7 +2418,7 @@ impl ConversationEditor { } } - fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext) { + fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext) { let editor = self.editor.read(cx); let conversation = self.conversation.read(cx); if editor.selections.count() == 1 { @@ -2828,7 +2829,7 @@ impl InlineAssistant { cx.notify(); } - fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext) { cx.emit(InlineAssistantEvent::Canceled); } diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index a5fa187d24acc93af3b2ff64dbf2ef96fffa7ea3..6f1ad6e4a335e65c58aa20ac19a859f0e849b068 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -8,9 +8,11 @@ use std::{ use call::ActiveCall; use editor::{ + actions::{ + ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Redo, Rename, ToggleCodeActions, Undo, + }, test::editor_test_context::{AssertionContextManager, EditorTestContext}, - ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToggleCodeActions, - Undo, + Editor, }; use futures::StreamExt; use gpui::{TestAppContext, VisualContext, VisualTestContext}; @@ -217,7 +219,8 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( editor_cx_b.set_selections_state(indoc! {" Some textˇ "}); - editor_cx_a.update_editor(|editor, cx| editor.newline_above(&editor::NewlineAbove, cx)); + editor_cx_a + .update_editor(|editor, cx| editor.newline_above(&editor::actions::NewlineAbove, cx)); executor.run_until_parked(); editor_cx_a.assert_editor_state(indoc! {" ˇ @@ -237,7 +240,8 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( Some textˇ "}); - editor_cx_a.update_editor(|editor, cx| editor.newline_below(&editor::NewlineBelow, cx)); + editor_cx_a + .update_editor(|editor, cx| editor.newline_below(&editor::actions::NewlineBelow, cx)); executor.run_until_parked(); editor_cx_a.assert_editor_state(indoc! {" diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index af184d7d02a3458deddb7bfdbf1f77be48d70790..d3565eb7ca32797a65db84a3bf3878db5caa771a 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1229,7 +1229,9 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont }); // When client B moves, it automatically stops following client A. - editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); + editor_b2.update(cx_b, |editor, cx| { + editor.move_right(&editor::actions::MoveRight, cx) + }); assert_eq!( workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), None diff --git a/crates/copilot_ui/src/copilot_button.rs b/crates/copilot_ui/src/copilot_button.rs index e5a1a942358a20c72fbb1037413796aeb84be77a..9dc4e75cb1cced8c5097bb3b7ee474fba9ed2249 100644 --- a/crates/copilot_ui/src/copilot_button.rs +++ b/crates/copilot_ui/src/copilot_button.rs @@ -1,7 +1,7 @@ use crate::sign_in::CopilotCodeVerification; use anyhow::Result; use copilot::{Copilot, SignOut, Status}; -use editor::{scroll::autoscroll::Autoscroll, Editor}; +use editor::{scroll::Autoscroll, Editor}; use fs::Fs; use gpui::{ div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement, diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index ca701e626e7f02284e22f553b507ca55a09524a5..8504d3de5e6ff6a2531f7a146db230ee9e840d14 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -8,7 +8,7 @@ use editor::{ diagnostic_block_renderer, display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock}, highlight_diagnostic_message, - scroll::autoscroll::Autoscroll, + scroll::Autoscroll, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, }; use futures::future::try_join_all; diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 462718c0f345268131cd04867d2bff9f48a451a0..d823ad52afc271e6789dd6f16724cc74c70ab227 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -80,7 +80,7 @@ impl Render for DiagnosticIndicator { Button::new("diagnostic_message", message) .label_size(LabelSize::Small) .tooltip(|cx| { - Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx) + Tooltip::for_action("Next Diagnostic", &editor::actions::GoToDiagnostic, cx) }) .on_click(cx.listener(|this, _, cx| { this.go_to_next_diagnostic(cx); diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs new file mode 100644 index 0000000000000000000000000000000000000000..9532bb642d85b15ae5cd8edf68e2338b1cefa174 --- /dev/null +++ b/crates/editor/src/actions.rs @@ -0,0 +1,218 @@ +//! This module contains all actions supported by [`Editor`]. +use super::*; + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct SelectNext { + #[serde(default)] + pub replace_newest: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct SelectPrevious { + #[serde(default)] + pub replace_newest: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct SelectAllMatches { + #[serde(default)] + pub replace_newest: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct SelectToBeginningOfLine { + #[serde(default)] + pub(super) stop_at_soft_wraps: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct MovePageUp { + #[serde(default)] + pub(super) center_cursor: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct MovePageDown { + #[serde(default)] + pub(super) center_cursor: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct SelectToEndOfLine { + #[serde(default)] + pub(super) stop_at_soft_wraps: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct ToggleCodeActions { + #[serde(default)] + pub deployed_from_indicator: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct ConfirmCompletion { + #[serde(default)] + pub item_ix: Option, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct ConfirmCodeAction { + #[serde(default)] + pub item_ix: Option, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct ToggleComments { + #[serde(default)] + pub advance_downwards: bool, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct FoldAt { + pub buffer_row: u32, +} + +#[derive(PartialEq, Clone, Deserialize, Default)] +pub struct UnfoldAt { + pub buffer_row: u32, +} +impl_actions!( + editor, + [ + SelectNext, + SelectPrevious, + SelectAllMatches, + SelectToBeginningOfLine, + MovePageUp, + MovePageDown, + SelectToEndOfLine, + ToggleCodeActions, + ConfirmCompletion, + ConfirmCodeAction, + ToggleComments, + FoldAt, + UnfoldAt + ] +); + +gpui::actions!( + editor, + [ + AddSelectionAbove, + AddSelectionBelow, + Backspace, + Cancel, + ConfirmRename, + ContextMenuFirst, + ContextMenuLast, + ContextMenuNext, + ContextMenuPrev, + ConvertToKebabCase, + ConvertToLowerCamelCase, + ConvertToLowerCase, + ConvertToSnakeCase, + ConvertToTitleCase, + ConvertToUpperCamelCase, + ConvertToUpperCase, + Copy, + CopyHighlightJson, + CopyPath, + CopyRelativePath, + Cut, + CutToEndOfLine, + Delete, + DeleteLine, + DeleteToBeginningOfLine, + DeleteToEndOfLine, + DeleteToNextSubwordEnd, + DeleteToNextWordEnd, + DeleteToPreviousSubwordStart, + DeleteToPreviousWordStart, + DuplicateLine, + ExpandMacroRecursively, + FindAllReferences, + Fold, + FoldSelectedRanges, + Format, + GoToDefinition, + GoToDefinitionSplit, + GoToDiagnostic, + GoToHunk, + GoToPrevDiagnostic, + GoToPrevHunk, + GoToTypeDefinition, + GoToTypeDefinitionSplit, + HalfPageDown, + HalfPageUp, + Hover, + Indent, + JoinLines, + LineDown, + LineUp, + MoveDown, + MoveLeft, + MoveLineDown, + MoveLineUp, + MoveRight, + MoveToBeginning, + MoveToBeginningOfLine, + MoveToEnclosingBracket, + MoveToEnd, + MoveToEndOfLine, + MoveToEndOfParagraph, + MoveToNextSubwordEnd, + MoveToNextWordEnd, + MoveToPreviousSubwordStart, + MoveToPreviousWordStart, + MoveToStartOfParagraph, + MoveUp, + Newline, + NewlineAbove, + NewlineBelow, + NextScreen, + OpenExcerpts, + Outdent, + PageDown, + PageUp, + Paste, + Redo, + RedoSelection, + Rename, + RestartLanguageServer, + RevealInFinder, + ReverseLines, + ScrollCursorBottom, + ScrollCursorCenter, + ScrollCursorTop, + SelectAll, + SelectDown, + SelectLargerSyntaxNode, + SelectLeft, + SelectLine, + SelectRight, + SelectSmallerSyntaxNode, + SelectToBeginning, + SelectToEnd, + SelectToEndOfParagraph, + SelectToNextSubwordEnd, + SelectToNextWordEnd, + SelectToPreviousSubwordStart, + SelectToPreviousWordStart, + SelectToStartOfParagraph, + SelectUp, + ShowCharacterPalette, + ShowCompletions, + ShuffleLines, + SortLinesCaseInsensitive, + SortLinesCaseSensitive, + SplitSelectionIntoLines, + Tab, + TabPrev, + ToggleInlayHints, + ToggleSoftWrap, + Transpose, + Undo, + UndoSelection, + UnfoldLines, + ] +); diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 4f2d5179dbd08fedd38f46ea23f919d6c30147c8..7ab5b0ff2abe75b6a35ab2ed120c2dee55e489af 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -30,7 +30,8 @@ pub use block_map::{ }; pub use self::fold_map::{Fold, FoldPoint}; -pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; +pub use self::inlay_map::{InlayOffset, InlayPoint}; +pub(crate) use inlay_map::Inlay; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { @@ -220,7 +221,7 @@ impl DisplayMap { .insert(Some(type_id), Arc::new((style, ranges))); } - pub fn highlight_inlays( + pub(crate) fn highlight_inlays( &mut self, type_id: TypeId, highlights: Vec, @@ -258,11 +259,11 @@ impl DisplayMap { .update(cx, |map, cx| map.set_wrap_width(width, cx)) } - pub fn current_inlays(&self) -> impl Iterator { + pub(crate) fn current_inlays(&self) -> impl Iterator { self.inlay_map.current_inlays() } - pub fn splice_inlays( + pub(crate) fn splice_inlays( &mut self, to_remove: Vec, to_insert: Vec, @@ -306,7 +307,7 @@ impl DisplayMap { } #[derive(Debug, Default)] -pub struct Highlights<'a> { +pub(crate) struct Highlights<'a> { pub text_highlights: Option<&'a TextHighlights>, pub inlay_highlights: Option<&'a InlayHighlights>, pub inlay_highlight_style: Option, @@ -880,8 +881,9 @@ impl DisplaySnapshot { self.text_highlights.get(&Some(type_id)).cloned() } + #[allow(unused)] #[cfg(any(test, feature = "test-support"))] - pub fn inlay_highlights( + pub(crate) fn inlay_highlights( &self, ) -> Option<&HashMap> { let type_id = TypeId::of::(); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 6eb0d05bfe84c4b487bb65bfc97ee49bcaff1736..dbbcbccb6e52b1596c408fb45d82c4798a871f46 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -582,7 +582,7 @@ impl BlockSnapshot { .collect() } - pub fn chunks<'a>( + pub(crate) fn chunks<'a>( &'a self, rows: Range, language_aware: bool, diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 4dad2d52aeb5236ec2936b8bdcaf3b06f760cc84..7c6eeb444eb69ca7557a6d55ab4b4b1125b8b078 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -655,7 +655,7 @@ impl FoldSnapshot { } } - pub fn chunks<'a>( + pub(crate) fn chunks<'a>( &'a self, range: Range, language_aware: bool, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 84fad96a48026c4ab7a08f3f6674a0a2958b89a7..c0d5198ddd9734c5a22cde9ee22e9ca837c23ebc 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -35,8 +35,8 @@ enum Transform { } #[derive(Debug, Clone)] -pub struct Inlay { - pub id: InlayId, +pub(crate) struct Inlay { + pub(crate) id: InlayId, pub position: Anchor, pub text: text::Rope, } @@ -1016,7 +1016,7 @@ impl InlaySnapshot { (line_end - line_start) as u32 } - pub fn chunks<'a>( + pub(crate) fn chunks<'a>( &'a self, range: Range, language_aware: bool, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index ce2e5ee3d9eb66c1e4a769dacdb9ff8c357a1ff3..39f9a2315bd928e0d6b2a67a262957911d586cbd 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -568,7 +568,7 @@ impl WrapSnapshot { Patch::new(wrap_edits) } - pub fn chunks<'a>( + pub(crate) fn chunks<'a>( &'a self, rows: Range, language_aware: bool, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 30b0a73d37e093bcb456d4fa6cf8e0c2ff98d5ff..378607a4fb0633695aaaaa3c7df630eb1ed41073 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1,3 +1,18 @@ +#![allow(rustdoc::private_intra_doc_links)] +//! This is the place where everything editor-related is stored (data-wise) and displayed (ui-wise). +//! The main point of interest in this crate is [`Editor`] type, which is used in every other Zed part as a user input element. +//! It comes in different flavors: single line, multiline and a fixed height one. +//! +//! Editor contains of multiple large submodules: +//! * [`element`] — the place where all rendering happens +//! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them. +//! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.). +//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly. +//! +//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). +//! +//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides it's behaviour. +pub mod actions; mod blink_manager; pub mod display_map; mod editor_settings; @@ -14,13 +29,14 @@ pub mod movement; mod persistence; mod rust_analyzer_ext; pub mod scroll; -pub mod selections_collection; +mod selections_collection; #[cfg(test)] mod editor_tests; #[cfg(any(test, feature = "test-support"))] pub mod test; use ::git::diff::DiffHunk; +pub(crate) use actions::*; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; @@ -32,14 +48,13 @@ use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; pub use editor_settings::EditorSettings; -pub use element::{ - Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles, -}; +use element::LineWithInvisibles; +pub use element::{Cursor, EditorElement, HighlightedRange, HighlightedRangeLine}; use futures::FutureExt; use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ - actions, div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action, + div, impl_actions, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, MouseButton, @@ -51,7 +66,7 @@ use hover_popover::{hide_hover, HoverState}; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; -pub use language::{char_kind, CharKind}; +use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CodeAction, @@ -74,9 +89,7 @@ use parking_lot::RwLock; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; use rpc::proto::{self, *}; -use scroll::{ - autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, -}; +use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; @@ -113,10 +126,12 @@ const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); +#[doc(hidden)] pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +#[doc(hidden)] pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); -pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); +pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub fn render_parsed_markdown( element_id: impl Into, @@ -181,103 +196,8 @@ pub fn render_parsed_markdown( }) } -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectNext { - #[serde(default)] - pub replace_newest: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectPrevious { - #[serde(default)] - pub replace_newest: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectAllMatches { - #[serde(default)] - pub replace_newest: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectToBeginningOfLine { - #[serde(default)] - stop_at_soft_wraps: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct MovePageUp { - #[serde(default)] - center_cursor: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct MovePageDown { - #[serde(default)] - center_cursor: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct SelectToEndOfLine { - #[serde(default)] - stop_at_soft_wraps: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct ToggleCodeActions { - #[serde(default)] - pub deployed_from_indicator: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct ConfirmCompletion { - #[serde(default)] - pub item_ix: Option, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct ConfirmCodeAction { - #[serde(default)] - pub item_ix: Option, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct ToggleComments { - #[serde(default)] - pub advance_downwards: bool, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct FoldAt { - pub buffer_row: u32, -} - -#[derive(PartialEq, Clone, Deserialize, Default)] -pub struct UnfoldAt { - pub buffer_row: u32, -} - -impl_actions!( - editor, - [ - SelectNext, - SelectPrevious, - SelectAllMatches, - SelectToBeginningOfLine, - MovePageUp, - MovePageDown, - SelectToEndOfLine, - ToggleCodeActions, - ConfirmCompletion, - ConfirmCodeAction, - ToggleComments, - FoldAt, - UnfoldAt - ] -); - #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum InlayId { +pub(crate) enum InlayId { Suggestion(usize), Hint(usize), } @@ -291,128 +211,6 @@ impl InlayId { } } -actions!( - editor, - [ - AddSelectionAbove, - AddSelectionBelow, - Backspace, - Cancel, - ConfirmRename, - ContextMenuFirst, - ContextMenuLast, - ContextMenuNext, - ContextMenuPrev, - ConvertToKebabCase, - ConvertToLowerCamelCase, - ConvertToLowerCase, - ConvertToSnakeCase, - ConvertToTitleCase, - ConvertToUpperCamelCase, - ConvertToUpperCase, - Copy, - CopyHighlightJson, - CopyPath, - CopyRelativePath, - Cut, - CutToEndOfLine, - Delete, - DeleteLine, - DeleteToBeginningOfLine, - DeleteToEndOfLine, - DeleteToNextSubwordEnd, - DeleteToNextWordEnd, - DeleteToPreviousSubwordStart, - DeleteToPreviousWordStart, - DuplicateLine, - ExpandMacroRecursively, - FindAllReferences, - Fold, - FoldSelectedRanges, - Format, - GoToDefinition, - GoToDefinitionSplit, - GoToDiagnostic, - GoToHunk, - GoToPrevDiagnostic, - GoToPrevHunk, - GoToTypeDefinition, - GoToTypeDefinitionSplit, - HalfPageDown, - HalfPageUp, - Hover, - Indent, - JoinLines, - LineDown, - LineUp, - MoveDown, - MoveLeft, - MoveLineDown, - MoveLineUp, - MoveRight, - MoveToBeginning, - MoveToBeginningOfLine, - MoveToEnclosingBracket, - MoveToEnd, - MoveToEndOfLine, - MoveToEndOfParagraph, - MoveToNextSubwordEnd, - MoveToNextWordEnd, - MoveToPreviousSubwordStart, - MoveToPreviousWordStart, - MoveToStartOfParagraph, - MoveUp, - Newline, - NewlineAbove, - NewlineBelow, - NextScreen, - OpenExcerpts, - Outdent, - PageDown, - PageUp, - Paste, - Redo, - RedoSelection, - Rename, - RestartLanguageServer, - RevealInFinder, - ReverseLines, - ScrollCursorBottom, - ScrollCursorCenter, - ScrollCursorTop, - SelectAll, - SelectDown, - SelectLargerSyntaxNode, - SelectLeft, - SelectLine, - SelectRight, - SelectSmallerSyntaxNode, - SelectToBeginning, - SelectToEnd, - SelectToEndOfParagraph, - SelectToNextSubwordEnd, - SelectToNextWordEnd, - SelectToPreviousSubwordStart, - SelectToPreviousWordStart, - SelectToStartOfParagraph, - SelectUp, - ShowCharacterPalette, - ShowCompletions, - ShuffleLines, - SortLinesCaseInsensitive, - SortLinesCaseSensitive, - SplitSelectionIntoLines, - Tab, - TabPrev, - ToggleInlayHints, - ToggleSoftWrap, - Transpose, - Undo, - UndoSelection, - UnfoldLines, - ] -); - enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} @@ -489,7 +287,7 @@ pub enum SelectPhase { } #[derive(Clone, Debug)] -pub enum SelectMode { +pub(crate) enum SelectMode { Character, Word(Range), Line(Range), @@ -760,6 +558,7 @@ struct SnippetState { active_index: usize, } +#[doc(hidden)] pub struct RenameState { pub range: Range, pub old_name: Arc, @@ -1499,7 +1298,7 @@ impl CodeActionsMenu { } } -pub struct CopilotState { +pub(crate) struct CopilotState { excerpt_id: Option, pending_refresh: Task>, pending_cycling_refresh: Task>, @@ -1619,15 +1418,13 @@ pub struct ClipboardSelection { } #[derive(Debug)] -pub struct NavigationData { +pub(crate) struct NavigationData { cursor_anchor: Anchor, cursor_position: Point, scroll_anchor: ScrollAnchor, scroll_top_row: u32, } -pub struct EditorCreated(pub View); - enum GotoDefinitionKind { Symbol, Type, @@ -8125,7 +7922,7 @@ impl Editor { } } - pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext) { + pub fn fold(&mut self, _: &actions::Fold, cx: &mut ViewContext) { let mut fold_ranges = Vec::new(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -8484,7 +8281,7 @@ impl Editor { cx.notify(); } - pub fn highlight_inlay_background( + pub(crate) fn highlight_inlay_background( &mut self, ranges: Vec, color_fetcher: fn(&ThemeColors) -> Hsla, @@ -8691,7 +8488,7 @@ impl Editor { cx.notify(); } - pub fn highlight_inlays( + pub(crate) fn highlight_inlays( &mut self, highlights: Vec, style: HighlightStyle, @@ -9899,7 +9696,7 @@ pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, V (text_without_backticks.into(), code_ranges) } -pub fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla { +fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla { match (severity, valid) { (DiagnosticSeverity::ERROR, true) => colors.error, (DiagnosticSeverity::ERROR, false) => colors.error, @@ -9958,7 +9755,7 @@ pub fn styled_runs_for_code_label<'a>( }) } -pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator + 'a { +pub(crate) fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator + 'a { let mut index = 0; let mut codepoints = text.char_indices().peekable(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b82bd55bcf5e898299ca8a3a0e1d88cf37c72367..64c0fdeb64045abef5932eb76eff0e33c3a39c4a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2599,7 +2599,7 @@ impl EditorElement { } #[derive(Debug)] -pub struct LineWithInvisibles { +pub(crate) struct LineWithInvisibles { pub line: ShapedLine, invisibles: Vec, } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 59c6b8605c1001440999e3f6909db300cf33e392..73c9b5dcd754f208318db809fcec4c8ba430ba87 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1,3 +1,11 @@ +/// Stores and updates all data received from LSP textDocument/inlayHint requests. +/// Has nothing to do with other inlays, e.g. copilot suggestions — those are stored elsewhere. +/// On every update, cache may query for more inlay hints and update inlays on the screen. +/// +/// Inlays stored on screen are in [`crate::display_map::inlay_map`] and this cache is the only way to update any inlay hint data in the visible hints in the inlay map. +/// For determining the update to the `inlay_map`, the cache requires a list of visible inlay hints — all other hints are not relevant and their separate updates are not influencing the cache work. +/// +/// Due to the way the data is stored for both visible inlays and the cache, every inlay (and inlay hint) collection is editor-specific, so a single buffer may have multiple sets of inlays of open on different panes. use std::{ cmp, ops::{ControlFlow, Range}, @@ -39,7 +47,7 @@ struct TasksForRanges { } #[derive(Debug)] -pub struct CachedExcerptHints { +struct CachedExcerptHints { version: usize, buffer_version: Global, buffer_id: u64, @@ -47,15 +55,30 @@ pub struct CachedExcerptHints { hints_by_id: HashMap, } +/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts. #[derive(Debug, Clone, Copy)] -pub enum InvalidationStrategy { +pub(super) enum InvalidationStrategy { + /// Hints reset is requested by the LSP server. + /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. + /// + /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise. RefreshRequested, + /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. + /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence. BufferEdited, + /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. + /// No invalidation should be done at all, all new hints are added to the cache. + /// + /// A special case is the settings change: in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other). + /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen. None, } -#[derive(Debug, Default)] -pub struct InlaySplice { +/// A splice to send into the `inlay_map` for updating the visible inlays on the screen. +/// "Visible" inlays may not be displayed in the buffer right away, but those are ready to be displayed on further buffer scroll, pane item activations, etc. right away without additional LSP queries or settings changes. +/// The data in the cache is never used directly for displaying inlays on the screen, to avoid races with updates from LSP queries and sync overhead. +/// Splice is picked to help avoid extra hint flickering and "jumps" on the screen. +pub(super) struct InlaySplice { pub to_remove: Vec, pub to_insert: Vec, } @@ -237,7 +260,7 @@ impl TasksForRanges { } impl InlayHintCache { - pub fn new(inlay_hint_settings: InlayHintSettings) -> Self { + pub(super) fn new(inlay_hint_settings: InlayHintSettings) -> Self { Self { allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(), enabled: inlay_hint_settings.enabled, @@ -248,7 +271,10 @@ impl InlayHintCache { } } - pub fn update_settings( + /// Checks inlay hint settings for enabled hint kinds and general enabled state. + /// Generates corresponding inlay_map splice updates on settings changes. + /// Does not update inlay hint cache state on disabling or inlay hint kinds change: only reenabling forces new LSP queries. + pub(super) fn update_settings( &mut self, multi_buffer: &Model, new_hint_settings: InlayHintSettings, @@ -299,7 +325,11 @@ impl InlayHintCache { } } - pub fn spawn_hint_refresh( + /// If needed, queries LSP for new inlay hints, using the invalidation strategy given. + /// To reduce inlay hint jumping, attempts to query a visible range of the editor(s) first, + /// followed by the delayed queries of the same range above and below the visible one. + /// This way, concequent refresh invocations are less likely to trigger LSP queries for the invisible ranges. + pub(super) fn spawn_hint_refresh( &mut self, reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, @@ -460,7 +490,11 @@ impl InlayHintCache { } } - pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> Option { + /// Completely forget of certain excerpts that were removed from the multibuffer. + pub(super) fn remove_excerpts( + &mut self, + excerpts_removed: Vec, + ) -> Option { let mut to_remove = Vec::new(); for excerpt_to_remove in excerpts_removed { self.update_tasks.remove(&excerpt_to_remove); @@ -480,7 +514,7 @@ impl InlayHintCache { } } - pub fn clear(&mut self) { + pub(super) fn clear(&mut self) { if !self.update_tasks.is_empty() || !self.hints.is_empty() { self.version += 1; } @@ -488,7 +522,7 @@ impl InlayHintCache { self.hints.clear(); } - pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { + pub(super) fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { self.hints .get(&excerpt_id)? .read() @@ -516,7 +550,8 @@ impl InlayHintCache { self.version } - pub fn spawn_hint_resolve( + /// Queries a certain hint from the cache for extra data via the LSP resolve request. + pub(super) fn spawn_hint_resolve( &self, buffer_id: u64, excerpt_id: ExcerptId, @@ -1199,7 +1234,7 @@ pub mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use crate::{ - scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, + scroll::{scroll_amount::ScrollAmount, Autoscroll}, ExcerptRange, }; use futures::StreamExt; diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 04dcf9301500024230e55e30d8b20ee4b04b2c9f..c4da7fcd38729599e5782e34ca71020e62c5e1e1 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -69,7 +69,7 @@ pub enum GoToDefinitionLink { } #[derive(Debug, Clone, PartialEq, Eq)] -pub struct InlayHighlight { +pub(crate) struct InlayHighlight { pub inlay: InlayId, pub inlay_position: Anchor, pub range: Range, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 72441974c3788d659084c19503ffc22740bfd005..0cf8ac7440ecbba8cd1e841360c69e302ad71b37 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,3 +1,6 @@ +//! Movement module contains helper functions for calculating intended position +//! in editor given a given motion (e.g. it handles converting a "move left" command into coordinates in editor). It is exposed mostly for use by vim crate. + use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint}; use gpui::{px, Pixels, TextSystem}; @@ -5,6 +8,9 @@ use language::Point; use std::{ops::Range, sync::Arc}; +/// Defines search strategy for items in `movement` module. +/// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas +/// `FindRange::MultiLine` keeps going until the end of a string. #[derive(Debug, PartialEq)] pub enum FindRange { SingleLine, @@ -14,11 +20,13 @@ pub enum FindRange { /// TextLayoutDetails encompasses everything we need to move vertically /// taking into account variable width characters. pub struct TextLayoutDetails { - pub text_system: Arc, - pub editor_style: EditorStyle, - pub rem_size: Pixels, + pub(crate) text_system: Arc, + pub(crate) editor_style: EditorStyle, + pub(crate) rem_size: Pixels, } +/// Returns a column to the left of the current point, wrapping +/// to the previous line if that point is at the start of line. pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; @@ -29,6 +37,8 @@ pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { map.clip_point(point, Bias::Left) } +/// Returns a column to the left of the current point, doing nothing if +/// that point is already at the start of line. pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; @@ -36,6 +46,8 @@ pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displa map.clip_point(point, Bias::Left) } +/// Returns a column to the right of the current point, wrapping +/// to the next line if that point is at the end of line. pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { let max_column = map.line_len(point.row()); if point.column() < max_column { @@ -47,11 +59,14 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { map.clip_point(point, Bias::Right) } +/// Returns a column to the right of the current point, not performing any wrapping +/// if that point is already at the end of line. pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { *point.column_mut() += 1; map.clip_point(point, Bias::Right) } +/// Returns a display point for the preceding displayed line (which might be a soft-wrapped line). pub fn up( map: &DisplaySnapshot, start: DisplayPoint, @@ -69,6 +84,7 @@ pub fn up( ) } +/// Returns a display point for the next displayed line (which might be a soft-wrapped line). pub fn down( map: &DisplaySnapshot, start: DisplayPoint, @@ -86,7 +102,7 @@ pub fn down( ) } -pub fn up_by_rows( +pub(crate) fn up_by_rows( map: &DisplaySnapshot, start: DisplayPoint, row_count: u32, @@ -125,7 +141,7 @@ pub fn up_by_rows( ) } -pub fn down_by_rows( +pub(crate) fn down_by_rows( map: &DisplaySnapshot, start: DisplayPoint, row_count: u32, @@ -161,6 +177,10 @@ pub fn down_by_rows( ) } +/// Returns a position of the start of line. +/// If `stop_at_soft_boundaries` is true, the returned position is that of the +/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped). +/// Otherwise it's always going to be the start of a logical line. pub fn line_beginning( map: &DisplaySnapshot, display_point: DisplayPoint, @@ -177,6 +197,10 @@ pub fn line_beginning( } } +/// Returns the last indented position on a given line. +/// If `stop_at_soft_boundaries` is true, the returned [`DisplayPoint`] is that of a +/// displayed line (e.g. if there's soft wrap it's gonna be returned), +/// otherwise it's always going to be a start of a logical line. pub fn indented_line_beginning( map: &DisplaySnapshot, display_point: DisplayPoint, @@ -201,6 +225,11 @@ pub fn indented_line_beginning( } } +/// Returns a position of the end of line. + +/// If `stop_at_soft_boundaries` is true, the returned position is that of the +/// displayed line (e.g. it could actually be in the middle of a text line if that line is soft-wrapped). +/// Otherwise it's always going to be the end of a logical line. pub fn line_end( map: &DisplaySnapshot, display_point: DisplayPoint, @@ -217,6 +246,8 @@ pub fn line_end( } } +/// Returns a position of the previous word boundary, where a word character is defined as either +/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS). pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let scope = map.buffer_snapshot.language_scope_at(raw_point); @@ -227,6 +258,9 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa }) } +/// Returns a position of the previous subword boundary, where a subword is defined as a run of +/// word characters of the same "subkind" - where subcharacter kinds are '_' character, +/// lowerspace characters and uppercase characters. pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let scope = map.buffer_snapshot.language_scope_at(raw_point); @@ -240,6 +274,8 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis }) } +/// Returns a position of the next word boundary, where a word character is defined as either +/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS). pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let scope = map.buffer_snapshot.language_scope_at(raw_point); @@ -250,6 +286,9 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint }) } +/// Returns a position of the next subword boundary, where a subword is defined as a run of +/// word characters of the same "subkind" - where subcharacter kinds are '_' character, +/// lowerspace characters and uppercase characters. pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { let raw_point = point.to_point(map); let scope = map.buffer_snapshot.language_scope_at(raw_point); @@ -263,6 +302,8 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo }) } +/// Returns a position of the start of the current paragraph, where a paragraph +/// is defined as a run of non-blank lines. pub fn start_of_paragraph( map: &DisplaySnapshot, display_point: DisplayPoint, @@ -290,6 +331,8 @@ pub fn start_of_paragraph( DisplayPoint::zero() } +/// Returns a position of the end of the current paragraph, where a paragraph +/// is defined as a run of non-blank lines. pub fn end_of_paragraph( map: &DisplaySnapshot, display_point: DisplayPoint, @@ -376,6 +419,9 @@ pub fn find_boundary( map.clip_point(offset.to_display_point(map), Bias::Right) } +/// Returns an iterator over the characters following a given offset in the [`DisplaySnapshot`]. +/// The returned value also contains a range of the start/end of a returned character in +/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer. pub fn chars_after( map: &DisplaySnapshot, mut offset: usize, @@ -387,6 +433,9 @@ pub fn chars_after( }) } +/// Returns a reverse iterator over the characters following a given offset in the [`DisplaySnapshot`]. +/// The returned value also contains a range of the start/end of a returned character in +/// the [`DisplaySnapshot`]. The offsets are relative to the start of a buffer. pub fn chars_before( map: &DisplaySnapshot, mut offset: usize, @@ -400,7 +449,7 @@ pub fn chars_before( }) } -pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { +pub(crate) fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { let raw_point = point.to_point(map); let scope = map.buffer_snapshot.language_scope_at(raw_point); let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); @@ -413,7 +462,10 @@ pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) } -pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range { +pub(crate) fn surrounding_word( + map: &DisplaySnapshot, + position: DisplayPoint, +) -> Range { let position = map .clip_point(position, Bias::Left) .to_offset(map, Bias::Left); @@ -429,6 +481,12 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range< start..end } +/// Returns a list of lines (represented as a [`DisplayPoint`] range) contained +/// within a passed range. +/// +/// The line ranges are **always* going to be in bounds of a requested range, which means that +/// the first and the last lines might not necessarily represent the +/// full range of a logical line (as their `.start`/`.end` values are clipped to those of a passed in range). pub fn split_display_range_by_lines( map: &DisplaySnapshot, range: Range, diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index bc5fe4bddd1b38d9445f69bd481145a2f3c884f7..f68004109e8889600c08b91ffe1edc5cc54ddee1 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -1,6 +1,6 @@ -pub mod actions; -pub mod autoscroll; -pub mod scroll_amount; +mod actions; +pub(crate) mod autoscroll; +pub(crate) mod scroll_amount; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, @@ -9,8 +9,10 @@ use crate::{ Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason, MultiBufferSnapshot, ToPoint, }; +pub use autoscroll::{Autoscroll, AutoscrollStrategy}; use gpui::{point, px, AppContext, Entity, Pixels, Task, ViewContext}; use language::{Bias, Point}; +pub use scroll_amount::ScrollAmount; use std::{ cmp::Ordering, time::{Duration, Instant}, @@ -18,11 +20,6 @@ use std::{ use util::ResultExt; use workspace::{ItemId, WorkspaceId}; -use self::{ - autoscroll::{Autoscroll, AutoscrollStrategy}, - scroll_amount::ScrollAmount, -}; - pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); pub const VERTICAL_SCROLL_MARGIN: f32 = 3.; const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 2a5ac568b79bb9df45489bf5e6b37f37ffcba25b..955b970540ca21d3d23c90740a508d2fe862b6af 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -175,7 +175,7 @@ impl Editor { true } - pub fn autoscroll_horizontally( + pub(crate) fn autoscroll_horizontally( &mut self, start_row: u32, viewport_width: Pixels, diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 8d71916210a6897b4fa649eef2ebbe05a041acb9..96f9507dd2aace6bbd25bbda62a25d68f07c8e3b 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -99,7 +99,7 @@ impl SelectionsCollection { .map(|pending| pending.map(|p| p.summary::(&self.buffer(cx)))) } - pub fn pending_mode(&self) -> Option { + pub(crate) fn pending_mode(&self) -> Option { self.pending.as_ref().map(|pending| pending.mode.clone()) } @@ -398,7 +398,7 @@ impl<'a> MutableSelectionsCollection<'a> { } } - pub fn set_pending_anchor_range(&mut self, range: Range, mode: SelectMode) { + pub(crate) fn set_pending_anchor_range(&mut self, range: Range, mode: SelectMode) { self.collection.pending = Some(PendingSelection { selection: Selection { id: post_inc(&mut self.collection.next_selection_id), @@ -412,7 +412,11 @@ impl<'a> MutableSelectionsCollection<'a> { self.selections_changed = true; } - pub fn set_pending_display_range(&mut self, range: Range, mode: SelectMode) { + pub(crate) fn set_pending_display_range( + &mut self, + range: Range, + mode: SelectMode, + ) { let (start, end, reversed) = { let display_map = self.display_map(); let buffer = self.buffer(); @@ -448,7 +452,7 @@ impl<'a> MutableSelectionsCollection<'a> { self.selections_changed = true; } - pub fn set_pending(&mut self, selection: Selection, mode: SelectMode) { + pub(crate) fn set_pending(&mut self, selection: Selection, mode: SelectMode) { self.collection.pending = Some(PendingSelection { selection, mode }); self.selections_changed = true; } @@ -855,7 +859,7 @@ impl<'a> DerefMut for MutableSelectionsCollection<'a> { } // Panics if passed selections are not in order -pub fn resolve_multiple<'a, D, I>( +pub(crate) fn resolve_multiple<'a, D, I>( selections: I, snapshot: &MultiBufferSnapshot, ) -> impl 'a + Iterator> diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 022860ea4718dbf39717cc86c631721d5e86621b..8484843c87253c6b1aa6b801e46435048d1558a4 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1,5 +1,5 @@ use collections::HashMap; -use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; +use editor::{scroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ actions, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index b7e3f27fac257bab08ac1e85401cf7cb5a383dfc..0a74f1ac03f96f5ea121b14da67993f40a2ddfd1 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -1,4 +1,4 @@ -use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; +use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Editor}; use gpui::{ actions, div, prelude::*, AnyWindowHandle, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 1ffab2f3d3e4e6e34514e148b68b4fba48dd2aa5..b15da05e1737d3e21a7c3e84fb2a8e5184330be9 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -1,6 +1,6 @@ use anyhow::Result; use chrono::{Datelike, Local, NaiveTime, Timelike}; -use editor::scroll::autoscroll::Autoscroll; +use editor::scroll::Autoscroll; use editor::Editor; use gpui::{actions, AppContext, ViewContext, WindowContext}; use schemars::JsonSchema; diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 75b4305b58329a48cd19563fc659be6a27f09af0..b4e2b37e83fd52a26c63f97857a93e444323bc0a 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1,5 +1,5 @@ use collections::{HashMap, VecDeque}; -use editor::{Editor, EditorEvent, MoveToEnd}; +use editor::{actions::MoveToEnd, Editor, EditorEvent}; use futures::{channel::mpsc, StreamExt}; use gpui::{ actions, div, AnchorCorner, AnyElement, AppContext, Context, EventEmitter, FocusHandle, diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 5acc6bff7fb1472697bd95c363c2914470457ea3..be677b215b744c239e0a290ad54c7bf23bfca2d4 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -1,4 +1,4 @@ -use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; +use editor::{scroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div, EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 1f2112003974aeb95ba55202af87ec20e7e04331..53f78b8fbd45bbf208aa7cc537b4d5d3638ded40 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -1,6 +1,6 @@ use editor::{ - display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, - DisplayPoint, Editor, EditorMode, ToPoint, + display_map::ToDisplayPoint, scroll::Autoscroll, Anchor, AnchorRangeExt, DisplayPoint, Editor, + EditorMode, ToPoint, }; use fuzzy::StringMatch; use gpui::{ diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 4301b6e392df1af4d2f411fc6e51b50564ffb16f..79c158048ee7e08b79f9adcc1d31fa1eea587e44 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3,7 +3,7 @@ mod project_panel_settings; use settings::Settings; use db::kvp::KEY_VALUE_STORE; -use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; +use editor::{actions::Cancel, scroll::Autoscroll, Editor}; use file_associations::FileAssociations; use anyhow::{anyhow, Result}; diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 68a0721b4c54386f31176aef44fb6a0733b02524..3d3e2328954d628d9c61cef7d0fb2c49f27464cf 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -1,4 +1,4 @@ -use editor::{scroll::autoscroll::Autoscroll, styled_runs_for_code_label, Bias, Editor}; +use editor::{scroll::Autoscroll, styled_runs_for_code_label, Bias, Editor}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task, diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 865632142a48978f33a446a49fe65c20183cf832..3e49328c133231ef06bad3123467cd25af4fe97a 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -45,13 +45,13 @@ impl Render for QuickActionBar { "toggle inlay hints", IconName::InlayHint, editor.read(cx).inlay_hints_enabled(), - Box::new(editor::ToggleInlayHints), + Box::new(editor::actions::ToggleInlayHints), "Toggle Inlay Hints", { let editor = editor.clone(); move |_, cx| { editor.update(cx, |editor, cx| { - editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); + editor.toggle_inlay_hints(&editor::actions::ToggleInlayHints, cx); }); } }, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c4e3ea5b5cb4ff9719221632cd6bf8cdfed8f5fa..a1f0d9773be242818fa31cdb26a5a0de9e532dcd 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -7,7 +7,7 @@ use crate::{ ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, }; use collections::HashMap; -use editor::{Editor, EditorElement, EditorStyle, Tab}; +use editor::{actions::Tab, Editor, EditorElement, EditorStyle}; use futures::channel::oneshot; use gpui::{ actions, div, impl_actions, Action, AppContext, ClickEvent, EventEmitter, FocusableView, @@ -635,7 +635,7 @@ impl BufferSearchBar { registrar.register_handler(|this, action: &SelectAllMatches, cx| { this.select_all_matches(action, cx); }); - registrar.register_handler(|this, _: &editor::Cancel, cx| { + registrar.register_handler(|this, _: &editor::actions::Cancel, cx| { this.dismiss(&Dismiss, cx); }); registrar.register_handler_for_dismissed_search(|this, deploy, cx| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 098fa184e353ab58b05a6e5709b0eb62f1a5a16f..919129ef76be9ffb21f77b809b42304f7a910beb 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -7,8 +7,8 @@ use crate::{ use anyhow::{Context as _, Result}; use collections::HashMap; use editor::{ - items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, EditorEvent, - MultiBuffer, SelectAll, MAX_TAB_TITLE_LEN, + actions::SelectAll, items::active_match_index, scroll::Autoscroll, Anchor, Editor, EditorEvent, + MultiBuffer, MAX_TAB_TITLE_LEN, }; use editor::{EditorElement, EditorStyle}; use gpui::{ @@ -1383,11 +1383,11 @@ impl ProjectSearchBar { } } - fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { + fn tab(&mut self, _: &editor::actions::Tab, cx: &mut ViewContext) { self.cycle_field(Direction::Next, cx); } - fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext) { + fn tab_previous(&mut self, _: &editor::actions::TabPrev, cx: &mut ViewContext) { self.cycle_field(Direction::Prev, cx); } diff --git a/crates/storybook/src/stories/auto_height_editor.rs b/crates/storybook/src/stories/auto_height_editor.rs index 6d835155621c44993c701d83151f420b527de794..7b3cee92c8bad783eb8d1b71d2188caaf53cb521 100644 --- a/crates/storybook/src/stories/auto_height_editor.rs +++ b/crates/storybook/src/stories/auto_height_editor.rs @@ -10,7 +10,11 @@ pub struct AutoHeightEditorStory { impl AutoHeightEditorStory { pub fn new(cx: &mut WindowContext) -> View { - cx.bind_keys([KeyBinding::new("enter", editor::Newline, Some("Editor"))]); + cx.bind_keys([KeyBinding::new( + "enter", + editor::actions::Newline, + Some("Editor"), + )]); cx.new_view(|cx| Self { editor: cx.new_view(|cx| { let mut editor = Editor::auto_height(3, cx); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index db4b21627f13a3e050888ca1cf18a06488484b1a..16e1ca4a73a45e0d9eb76082894313cdecc8261b 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -2,7 +2,7 @@ mod persistence; pub mod terminal_element; pub mod terminal_panel; -use editor::{scroll::autoscroll::Autoscroll, Editor}; +use editor::{scroll::Autoscroll, Editor}; use gpui::{ div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, Pixels, @@ -357,7 +357,7 @@ impl TerminalView { } } - fn select_all(&mut self, _: &editor::SelectAll, cx: &mut ViewContext) { + fn select_all(&mut self, _: &editor::actions::SelectAll, cx: &mut ViewContext) { self.terminal.update(cx, |term, _| term.select_all()); cx.notify(); } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 34d8658afe54cd2efdbfcfa1ef5a469070488d75..f1b4853feb75f683c6aafdbeff7ba78cdb615274 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1,5 +1,5 @@ use command_palette::CommandInterceptResult; -use editor::{SortLinesCaseInsensitive, SortLinesCaseSensitive}; +use editor::actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}; use gpui::{impl_actions, Action, AppContext, ViewContext}; use serde_derive::Deserialize; use workspace::{SaveIntent, Workspace}; @@ -204,25 +204,31 @@ pub fn command_interceptor(mut query: &str, _: &AppContext) -> Option ("clist", diagnostics::Deploy.boxed_clone()), - "cc" => ("cc", editor::Hover.boxed_clone()), - "ll" => ("ll", editor::Hover.boxed_clone()), - "cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()), - "lne" | "lnex" | "lnext" => ("cnext", editor::GoToDiagnostic.boxed_clone()), - - "cpr" | "cpre" | "cprev" | "cprevi" | "cprevio" | "cpreviou" | "cprevious" => { - ("cprevious", editor::GoToPrevDiagnostic.boxed_clone()) + "cc" => ("cc", editor::actions::Hover.boxed_clone()), + "ll" => ("ll", editor::actions::Hover.boxed_clone()), + "cn" | "cne" | "cnex" | "cnext" => ("cnext", editor::actions::GoToDiagnostic.boxed_clone()), + "lne" | "lnex" | "lnext" => ("cnext", editor::actions::GoToDiagnostic.boxed_clone()), + + "cpr" | "cpre" | "cprev" | "cprevi" | "cprevio" | "cpreviou" | "cprevious" => ( + "cprevious", + editor::actions::GoToPrevDiagnostic.boxed_clone(), + ), + "cN" | "cNe" | "cNex" | "cNext" => { + ("cNext", editor::actions::GoToPrevDiagnostic.boxed_clone()) } - "cN" | "cNe" | "cNex" | "cNext" => ("cNext", editor::GoToPrevDiagnostic.boxed_clone()), - "lp" | "lpr" | "lpre" | "lprev" | "lprevi" | "lprevio" | "lpreviou" | "lprevious" => { - ("lprevious", editor::GoToPrevDiagnostic.boxed_clone()) + "lp" | "lpr" | "lpre" | "lprev" | "lprevi" | "lprevio" | "lpreviou" | "lprevious" => ( + "lprevious", + editor::actions::GoToPrevDiagnostic.boxed_clone(), + ), + "lN" | "lNe" | "lNex" | "lNext" => { + ("lNext", editor::actions::GoToPrevDiagnostic.boxed_clone()) } - "lN" | "lNe" | "lNex" | "lNext" => ("lNext", editor::GoToPrevDiagnostic.boxed_clone()), // modify the buffer (should accept [range]) "j" | "jo" | "joi" | "join" => ("join", JoinLines.boxed_clone()), "d" | "de" | "del" | "dele" | "delet" | "delete" | "dl" | "dell" | "delel" | "deletl" | "deletel" | "dp" | "dep" | "delp" | "delep" | "deletp" | "deletep" => { - ("delete", editor::DeleteLine.boxed_clone()) + ("delete", editor::actions::DeleteLine.boxed_clone()) } "sor" | "sor " | "sort" | "sort " => ("sort", SortLinesCaseSensitive.boxed_clone()), "sor i" | "sort i" => ("sort i", SortLinesCaseInsensitive.boxed_clone()), diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 7c272318287bf323e693121d0e0400cedaa83e75..a063d3747552780806244b08ae03d3476b31d6dc 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -1,5 +1,5 @@ use crate::{normal::repeat, state::Mode, Vim}; -use editor::{scroll::autoscroll::Autoscroll, Bias}; +use editor::{scroll::Autoscroll, Bias}; use gpui::{actions, Action, ViewContext}; use language::SelectionGoal; use workspace::Workspace; diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 73ba70d5add11f33d55e001e2637c05bcca0afe5..6215e4c16c0d1ebb10eb59358a87d1978bc45c5d 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,11 +1,10 @@ use editor::{ - char_kind, display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails}, - Bias, CharKind, DisplayPoint, ToOffset, + Bias, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, px, ViewContext, WindowContext}; -use language::{Point, Selection, SelectionGoal}; +use language::{char_kind, CharKind, Point, Selection, SelectionGoal}; use serde::Deserialize; use workspace::Workspace; diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index a8f2e5fa5a5995949652e8e3c1440f3cb9f90670..c21f54f2d39f2b5a84f65928c1c93505d335d3dc 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -18,7 +18,7 @@ use crate::{ Vim, }; use collections::HashSet; -use editor::scroll::autoscroll::Autoscroll; +use editor::scroll::Autoscroll; use editor::{Bias, DisplayPoint}; use gpui::{actions, ViewContext, WindowContext}; use language::SelectionGoal; diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index 22d09f8359f52060a7435cef321ca0c7f71bd37c..d94454891f7259767605b4335372bf26ba600157 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -1,4 +1,4 @@ -use editor::scroll::autoscroll::Autoscroll; +use editor::scroll::Autoscroll; use gpui::ViewContext; use language::{Bias, Point}; use workspace::Workspace; diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index bf2a25a98d5ccf622a7b802b84962d6aed6bf308..86b77038461577909ffe49445fe442cbb0e54f69 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -1,13 +1,12 @@ use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim}; use editor::{ - char_kind, display_map::DisplaySnapshot, movement::{self, FindRange, TextLayoutDetails}, - scroll::autoscroll::Autoscroll, - CharKind, DisplayPoint, + scroll::Autoscroll, + DisplayPoint, }; use gpui::WindowContext; -use language::Selection; +use language::{char_kind, CharKind, Selection}; pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { // Some motions ignore failure when switching to normal mode diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index b8105aeb8d7b7b22118179f2194fbcc551090ffb..ed9cdf19fad152a5e753942a8930c02d6666826a 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -1,6 +1,6 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim}; use collections::{HashMap, HashSet}; -use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias}; +use editor::{display_map::ToDisplayPoint, scroll::Autoscroll, Bias}; use gpui::WindowContext; use language::Point; diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 9fa06c48513817242ef496043b943c214eb3bbad..6353a881ed5d702bdecc6f0d1f5846032f4ea727 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -1,6 +1,6 @@ use std::ops::Range; -use editor::{scroll::autoscroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint}; +use editor::{scroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint}; use gpui::{impl_actions, ViewContext, WindowContext}; use language::{Bias, Point}; use serde::Deserialize; diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 169c2c4728f13a8cc8c6d2985079b254c735df23..a65a81665429b1a78f79e9710c22340e106234b0 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -1,8 +1,7 @@ use std::{borrow::Cow, cmp}; use editor::{ - display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection, - DisplayPoint, + display_map::ToDisplayPoint, movement, scroll::Autoscroll, ClipboardSelection, DisplayPoint, }; use gpui::{impl_actions, ViewContext}; use language::{Bias, SelectionGoal}; diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index a643c126ef4edbd00442578059b9db55cb3e5774..c6d1f0e6c352ceef55926363553d4314ce22f42d 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -12,7 +12,7 @@ actions!(vim, [Repeat, EndRepeat]); fn should_replay(action: &Box) -> bool { // skip so that we don't leave the character palette open - if editor::ShowCharacterPalette.partial_eq(&**action) { + if editor::actions::ShowCharacterPalette.partial_eq(&**action) { return false; } true diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 84a27e20cee6984ea04f16935f3e936a7333acf3..8c061582315a907c9a3dbf46b0a8ed0ce9cf3e1c 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -1,7 +1,7 @@ use crate::Vim; use editor::{ display_map::ToDisplayPoint, - scroll::{scroll_amount::ScrollAmount, VERTICAL_SCROLL_MARGIN}, + scroll::{ScrollAmount, VERTICAL_SCROLL_MARGIN}, DisplayPoint, Editor, }; use gpui::{actions, ViewContext}; diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 47d1647dc765b6c76896cb1bdfc3af0fff5a7f07..1e9361618c5cd6874a3d869dabe9ba01e411599c 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -1,13 +1,12 @@ use std::ops::Range; use editor::{ - char_kind, display_map::{DisplaySnapshot, ToDisplayPoint}, movement::{self, FindRange}, - Bias, CharKind, DisplayPoint, + Bias, DisplayPoint, }; use gpui::{actions, impl_actions, ViewContext, WindowContext}; -use language::Selection; +use language::{char_kind, CharKind, Selection}; use serde::Deserialize; use workspace::Workspace; diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1fd11167c6843206ddcef2b4ccee96a0f9886dae..797a271574800b72847bd733c32a4764e3ec023c 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -5,7 +5,7 @@ use collections::HashMap; use editor::{ display_map::{DisplaySnapshot, ToDisplayPoint}, movement, - scroll::autoscroll::Autoscroll, + scroll::Autoscroll, Bias, DisplayPoint, Editor, }; use gpui::{actions, ViewContext, WindowContext}; diff --git a/crates/zed/src/app_menus.rs b/crates/zed/src/app_menus.rs index 2aff05d884265aac2538973bf9d7b65ed3d54385..fc063a620f18ace58e8017e53bd08fb11ff5c894 100644 --- a/crates/zed/src/app_menus.rs +++ b/crates/zed/src/app_menus.rs @@ -53,39 +53,46 @@ pub fn app_menus() -> Vec> { Menu { name: "Edit", items: vec![ - MenuItem::os_action("Undo", editor::Undo, OsAction::Undo), - MenuItem::os_action("Redo", editor::Redo, OsAction::Redo), + MenuItem::os_action("Undo", editor::actions::Undo, OsAction::Undo), + MenuItem::os_action("Redo", editor::actions::Redo, OsAction::Redo), MenuItem::separator(), - MenuItem::os_action("Cut", editor::Cut, OsAction::Cut), - MenuItem::os_action("Copy", editor::Copy, OsAction::Copy), - MenuItem::os_action("Paste", editor::Paste, OsAction::Paste), + MenuItem::os_action("Cut", editor::actions::Cut, OsAction::Cut), + MenuItem::os_action("Copy", editor::actions::Copy, OsAction::Copy), + MenuItem::os_action("Paste", editor::actions::Paste, OsAction::Paste), MenuItem::separator(), MenuItem::action("Find", search::buffer_search::Deploy { focus: true }), MenuItem::action("Find In Project", workspace::NewSearch), MenuItem::separator(), - MenuItem::action("Toggle Line Comment", editor::ToggleComments::default()), - MenuItem::action("Emoji & Symbols", editor::ShowCharacterPalette), + MenuItem::action( + "Toggle Line Comment", + editor::actions::ToggleComments::default(), + ), + MenuItem::action("Emoji & Symbols", editor::actions::ShowCharacterPalette), ], }, Menu { name: "Selection", items: vec![ - MenuItem::os_action("Select All", editor::SelectAll, OsAction::SelectAll), - MenuItem::action("Expand Selection", editor::SelectLargerSyntaxNode), - MenuItem::action("Shrink Selection", editor::SelectSmallerSyntaxNode), + MenuItem::os_action( + "Select All", + editor::actions::SelectAll, + OsAction::SelectAll, + ), + MenuItem::action("Expand Selection", editor::actions::SelectLargerSyntaxNode), + MenuItem::action("Shrink Selection", editor::actions::SelectSmallerSyntaxNode), MenuItem::separator(), - MenuItem::action("Add Cursor Above", editor::AddSelectionAbove), - MenuItem::action("Add Cursor Below", editor::AddSelectionBelow), + MenuItem::action("Add Cursor Above", editor::actions::AddSelectionAbove), + MenuItem::action("Add Cursor Below", editor::actions::AddSelectionBelow), MenuItem::action( "Select Next Occurrence", - editor::SelectNext { + editor::actions::SelectNext { replace_newest: false, }, ), MenuItem::separator(), - MenuItem::action("Move Line Up", editor::MoveLineUp), - MenuItem::action("Move Line Down", editor::MoveLineDown), - MenuItem::action("Duplicate Selection", editor::DuplicateLine), + MenuItem::action("Move Line Up", editor::actions::MoveLineUp), + MenuItem::action("Move Line Down", editor::actions::MoveLineDown), + MenuItem::action("Duplicate Selection", editor::actions::DuplicateLine), ], }, Menu { @@ -124,13 +131,13 @@ pub fn app_menus() -> Vec> { MenuItem::action("Go to File", file_finder::Toggle), // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), MenuItem::action("Go to Symbol in Editor", outline::Toggle), - MenuItem::action("Go to Definition", editor::GoToDefinition), - MenuItem::action("Go to Type Definition", editor::GoToTypeDefinition), - MenuItem::action("Find All References", editor::FindAllReferences), + MenuItem::action("Go to Definition", editor::actions::GoToDefinition), + MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition), + MenuItem::action("Find All References", editor::actions::FindAllReferences), MenuItem::action("Go to Line/Column", go_to_line::Toggle), MenuItem::separator(), - MenuItem::action("Next Problem", editor::GoToDiagnostic), - MenuItem::action("Previous Problem", editor::GoToPrevDiagnostic), + MenuItem::action("Next Problem", editor::actions::GoToDiagnostic), + MenuItem::action("Previous Problem", editor::actions::GoToPrevDiagnostic), ], }, Menu { diff --git a/crates/zed/src/open_listener.rs b/crates/zed/src/open_listener.rs index 6db020a785788d1fe0d05cd2a4d10d937f2b5ac4..f3a10208d0d84e6f231944777c19502e9c84877f 100644 --- a/crates/zed/src/open_listener.rs +++ b/crates/zed/src/open_listener.rs @@ -1,7 +1,7 @@ use anyhow::{anyhow, Context, Result}; use cli::{ipc, IpcHandshake}; use cli::{ipc::IpcSender, CliRequest, CliResponse}; -use editor::scroll::autoscroll::Autoscroll; +use editor::scroll::Autoscroll; use editor::Editor; use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::{mpsc, oneshot}; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index afc06ad193659e7c015f2039f2efaa0f95a3297a..112c219d2d728b9a21b36d05554358d47a2b0aa2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -730,7 +730,7 @@ fn open_bundled_file( mod tests { use super::*; use assets::Assets; - use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor, EditorEvent}; + use editor::{scroll::Autoscroll, DisplayPoint, Editor, EditorEvent}; use gpui::{ actions, Action, AnyWindowHandle, AppContext, AssetSource, Entity, TestAppContext, VisualTestContext, WindowHandle, From 17018faa92d7543610f5efb8f2a6acca046a3727 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 17 Jan 2024 15:51:37 -0800 Subject: [PATCH 61/96] Update typos.toml --- typos.toml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/typos.toml b/typos.toml index 47eb2a0d9c493d4333d808310c99c2632f55e913..2881b65f4844bdbfb3e992d97afe54adbf2c3e7e 100644 --- a/typos.toml +++ b/typos.toml @@ -7,13 +7,15 @@ extend-exclude = [ "crates/zed/src/languages/glsl/*", # File suffixes aren't typos "assets/icons/file_icons/file_types.json", - # :/ - "crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql", + # Not our typos + "assets/themes/src/vscode/*", # Editor and file finder rely on partial typing and custom in-string syntax "crates/file_finder/src/file_finder.rs", "crates/editor/src/editor_tests.rs", + # :/ + "crates/collab/migrations/20231009181554_add_release_channel_to_rooms.sql", ] [default] extend-ignore-re = ["ba"] -check-filename = true \ No newline at end of file +check-filename = true From 078fd35f4fbc22c5de5604ebcf5561283532b9d2 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 15:52:54 -0800 Subject: [PATCH 62/96] WIP --- crates/editor/src/completions.rs | 28 +++++++++++++++ crates/editor/src/element.rs | 25 +++++++------ .../src/platform/mac/window_appearance.rs | 35 +++++++++++++++++++ 3 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 crates/editor/src/completions.rs create mode 100644 crates/gpui/src/platform/mac/window_appearance.rs diff --git a/crates/editor/src/completions.rs b/crates/editor/src/completions.rs new file mode 100644 index 0000000000000000000000000000000000000000..b9461364af44e686fc7207d2dd1c800a49156394 --- /dev/null +++ b/crates/editor/src/completions.rs @@ -0,0 +1,28 @@ +use futures::Future; +use gpui::Task; +use smallvec::{smallvec, SmallVec}; +use text::Anchor; + +use crate::Editor; + +struct Completions { + trigger_characters: SmallVec<[char; 1]>, + language: Option, + provider: Box Option>>, +} + +impl Completions { + fn new(f: impl Fn(&mut Editor, &Anchor, &str) -> Option> + 'static) -> Self { + Self { + trigger_characters: smallvec![], + language: None, + provider: Box::new(f), + } + } +} + +impl Editor { + /// Provide completions to the editor when the given character is typed + /// + fn provide_completions(config: Completions) {} +} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b82bd55bcf5e898299ca8a3a0e1d88cf37c72367..518e03c27ee1e3670c7b53df213bf9200dd56998 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2543,18 +2543,21 @@ impl EditorElement { move |event: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Bubble - && interactive_bounds.visibly_contains(&event.position, cx) { - editor.update(cx, |editor, cx| { - Self::mouse_up( - editor, - event, - &position_map, - text_bounds, - &stacking_order, - cx, - ) - }); + // if interactive_bounds.visibly_contains(&event.position, cx) { + editor.update(cx, |editor, cx| { + Self::mouse_up( + editor, + event, + &position_map, + text_bounds, + &stacking_order, + cx, + ) + }); + // } else { + + // } } } }); diff --git a/crates/gpui/src/platform/mac/window_appearance.rs b/crates/gpui/src/platform/mac/window_appearance.rs new file mode 100644 index 0000000000000000000000000000000000000000..2edc896289ef8056424a0399d38ff937155adad2 --- /dev/null +++ b/crates/gpui/src/platform/mac/window_appearance.rs @@ -0,0 +1,35 @@ +use crate::WindowAppearance; +use cocoa::{ + appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight}, + base::id, + foundation::NSString, +}; +use objc::{msg_send, sel, sel_impl}; +use std::ffi::CStr; + +impl WindowAppearance { + pub unsafe fn from_native(appearance: id) -> Self { + let name: id = msg_send![appearance, name]; + if name == NSAppearanceNameVibrantLight { + Self::VibrantLight + } else if name == NSAppearanceNameVibrantDark { + Self::VibrantDark + } else if name == NSAppearanceNameAqua { + Self::Light + } else if name == NSAppearanceNameDarkAqua { + Self::Dark + } else { + println!( + "unknown appearance: {:?}", + CStr::from_ptr(name.UTF8String()) + ); + Self::Light + } + } +} + +#[link(name = "AppKit", kind = "framework")] +extern "C" { + pub static NSAppearanceNameAqua: id; + pub static NSAppearanceNameDarkAqua: id; +} From ec2b299ecb8fe794385054cf04bd81f43d2e699a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:54:07 +0100 Subject: [PATCH 63/96] settings: Suggest fonts bundled in Zed (#4102) Fixes an issue where Zed Sans is not being suggested as a font. Release Notes: - N/A --- crates/gpui/src/platform/mac/text_system.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index 06179e126b6b779a56d25b1bf154eef37162a646..a77741074f13e1515198921b95a3a310e4d3d422 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -87,6 +87,9 @@ impl PlatformTextSystem for MacTextSystem { for descriptor in descriptors.into_iter() { names.insert(descriptor.display_name()); } + if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() { + names.extend(fonts_in_memory); + } names.into_iter().collect() } From 9a3709dbac494841f7f153d5d6b224c3cbbdb365 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 15:56:15 -0800 Subject: [PATCH 64/96] Revert vscode theme changes --- assets/themes/src/vscode/dracula/dracula.json | 2 +- .../themes/src/vscode/night-owl/night-owl-light.json | 6 +++--- assets/themes/src/vscode/night-owl/night-owl.json | 6 +++--- assets/themes/src/vscode/noctis/azureus.json | 2 +- assets/themes/src/vscode/noctis/bordo.json | 2 +- assets/themes/src/vscode/noctis/hibernus.json | 2 +- assets/themes/src/vscode/noctis/lilac.json | 2 +- assets/themes/src/vscode/noctis/lux.json | 2 +- assets/themes/src/vscode/noctis/minimus.json | 2 +- assets/themes/src/vscode/noctis/noctis.json | 2 +- assets/themes/src/vscode/noctis/obscuro.json | 2 +- assets/themes/src/vscode/noctis/sereno.json | 2 +- assets/themes/src/vscode/noctis/uva.json | 2 +- assets/themes/src/vscode/noctis/viola.json | 2 +- .../src/vscode/palenight/palenight-mild-contrast.json | 10 +++++----- .../src/vscode/palenight/palenight-operator.json | 10 +++++----- assets/themes/src/vscode/palenight/palenight.json | 10 +++++----- 17 files changed, 33 insertions(+), 33 deletions(-) diff --git a/assets/themes/src/vscode/dracula/dracula.json b/assets/themes/src/vscode/dracula/dracula.json index e9a29dec179baf2e518e6e82519adafe01ec879d..6604a094d5a194f74d378d25c38fb47a3f29c539 100644 --- a/assets/themes/src/vscode/dracula/dracula.json +++ b/assets/themes/src/vscode/dracula/dracula.json @@ -1024,7 +1024,7 @@ } }, { - "name": "SCSS attribute selector strings", + "name": "SCSS attibute selector strings", "scope": ["meta.attribute-selector.scss"], "settings": { "foreground": "#F1FA8C" diff --git a/assets/themes/src/vscode/night-owl/night-owl-light.json b/assets/themes/src/vscode/night-owl/night-owl-light.json index 627a55ac62b641b4b112fdf9388f6626fe64a018..81e0fc0092279aec3298ff2f77a81137e0340a68 100644 --- a/assets/themes/src/vscode/night-owl/night-owl-light.json +++ b/assets/themes/src/vscode/night-owl/night-owl-light.json @@ -892,14 +892,14 @@ } }, { - "name": "CoffeeScript Variable Assignment", + "name": "CoffeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#31e1eb" } }, { - "name": "CoffeeScript Parameter Function", + "name": "CoffeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#403f53" @@ -1708,7 +1708,7 @@ "keyword.operator.type", "keyword.operator", "keyword", - "punctuation.definition.string", + "punctuation.definintion.string", "punctuation", "variable.other.readwrite.js", "storage.type", diff --git a/assets/themes/src/vscode/night-owl/night-owl.json b/assets/themes/src/vscode/night-owl/night-owl.json index b16c22fb6afc415b17e6f78873b22ce2844eed5a..6d41b6299b4b911ff5d9952e56255a090c981704 100644 --- a/assets/themes/src/vscode/night-owl/night-owl.json +++ b/assets/themes/src/vscode/night-owl/night-owl.json @@ -926,14 +926,14 @@ } }, { - "name": "CoffeeScript Variable Assignment", + "name": "CoffeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#31e1eb" } }, { - "name": "CoffeeScript Parameter Function", + "name": "CoffeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#d6deeb" @@ -1817,7 +1817,7 @@ "keyword.operator.type", "keyword.operator", "keyword", - "punctuation.definition.string", + "punctuation.definintion.string", "punctuation", "variable.other.readwrite.js", "storage.type", diff --git a/assets/themes/src/vscode/noctis/azureus.json b/assets/themes/src/vscode/noctis/azureus.json index 300113a59d0877077534d362c6f5359ccc3e8c48..d550e74811b63e329f46142f6c5bff6e51584b61 100644 --- a/assets/themes/src/vscode/noctis/azureus.json +++ b/assets/themes/src/vscode/noctis/azureus.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/bordo.json b/assets/themes/src/vscode/noctis/bordo.json index 21c8a13511557dfa4cf09fd608af8b0f684ca7ae..a6c4853c3b078e7373f69ae1084ab7a9d5c47784 100644 --- a/assets/themes/src/vscode/noctis/bordo.json +++ b/assets/themes/src/vscode/noctis/bordo.json @@ -389,7 +389,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/hibernus.json b/assets/themes/src/vscode/noctis/hibernus.json index a2870e39058ad6ac9ecd722b28dde0823dafd926..a20a19289ea539b675cb42b3480eb6ff57e90e53 100644 --- a/assets/themes/src/vscode/noctis/hibernus.json +++ b/assets/themes/src/vscode/noctis/hibernus.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/lilac.json b/assets/themes/src/vscode/noctis/lilac.json index a54b4e3c50de40e6a06b58213e9b19eaed00953f..26e0fe422376496a3a91cc7d191a7057dc986073 100644 --- a/assets/themes/src/vscode/noctis/lilac.json +++ b/assets/themes/src/vscode/noctis/lilac.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/lux.json b/assets/themes/src/vscode/noctis/lux.json index 34dc89460e20e332a3e2eaee82ea2dec518a78c1..1f72b0e59cab91cb2255ee1438ace7b0102dfbcf 100644 --- a/assets/themes/src/vscode/noctis/lux.json +++ b/assets/themes/src/vscode/noctis/lux.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/minimus.json b/assets/themes/src/vscode/noctis/minimus.json index a347af76601a975f60ebf2f6c47a7f7d641f7885..88493d99d5993b6d72ef9a1a81228b8a82fe54c3 100644 --- a/assets/themes/src/vscode/noctis/minimus.json +++ b/assets/themes/src/vscode/noctis/minimus.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/noctis.json b/assets/themes/src/vscode/noctis/noctis.json index 61e90c46a9a37752743052edb6b7decb44e3871a..cc270fe526f10f3f1fea30464390f1d8d5a76c8e 100644 --- a/assets/themes/src/vscode/noctis/noctis.json +++ b/assets/themes/src/vscode/noctis/noctis.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/obscuro.json b/assets/themes/src/vscode/noctis/obscuro.json index 97e6f2d71a63b82bc688374f87e5333a5137d09c..26d1a02de84a4bad2056444302b5e9d83faa8312 100644 --- a/assets/themes/src/vscode/noctis/obscuro.json +++ b/assets/themes/src/vscode/noctis/obscuro.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/sereno.json b/assets/themes/src/vscode/noctis/sereno.json index b81da1edcecf18a3a4d52a58c18af2986c172485..05768aff356e40078973f34650143e9089e1971d 100644 --- a/assets/themes/src/vscode/noctis/sereno.json +++ b/assets/themes/src/vscode/noctis/sereno.json @@ -390,7 +390,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/uva.json b/assets/themes/src/vscode/noctis/uva.json index d4139faaf3a5c14356106d0b0da43501cfc554fd..6ccbff372b8a965d9451279380f07f069b8f8f67 100644 --- a/assets/themes/src/vscode/noctis/uva.json +++ b/assets/themes/src/vscode/noctis/uva.json @@ -389,7 +389,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/noctis/viola.json b/assets/themes/src/vscode/noctis/viola.json index 889d2dfc2a96e7707b2a72150f262fa14a61f548..4d474ad31173c6e8e5888faa44e501cbc0e95aaa 100644 --- a/assets/themes/src/vscode/noctis/viola.json +++ b/assets/themes/src/vscode/noctis/viola.json @@ -389,7 +389,7 @@ "source.reason variable.interpolation", "punctuation.definition.directive", "storage.type.modifier", - "keyword.other.class.fields", + "keyword.other.class.fileds", "source.toml entity.other.attribute-name", "source.css entity.name.tag.custom", "sharing.modifier", diff --git a/assets/themes/src/vscode/palenight/palenight-mild-contrast.json b/assets/themes/src/vscode/palenight/palenight-mild-contrast.json index 598a186692c928e5c7cfaabc1f1c073aea74d303..7533d90ffd5752e5ea160ab2c686a2173aa9e4eb 100644 --- a/assets/themes/src/vscode/palenight/palenight-mild-contrast.json +++ b/assets/themes/src/vscode/palenight/palenight-mild-contrast.json @@ -797,14 +797,14 @@ } }, { - "name": "CoffeeScript Variable Assignment", + "name": "CoffeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#89DDFF" } }, { - "name": "CoffeeScript Parameter Function", + "name": "CoffeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#bfc7d5" @@ -1523,14 +1523,14 @@ } }, { - "name": "handlebars entity attribute names", + "name": "handlebars enitity attribute names", "scope": "entity.other.attribute-name.handlebars", "settings": { "foreground": "#89DDFF" } }, { - "name": "handlebars entity attribute values", + "name": "handlebars enitity attribute values", "scope": "entity.other.attribute-value.handlebars variable.parameter.handlebars", "settings": { "foreground": "#7986E7" @@ -1558,7 +1558,7 @@ "keyword.operator.expression.in", "keyword.operator.type", "punctuation.section.embedded.js", - "punctuation.definition.string", + "punctuation.definintion.string", "punctuation" ], "settings": { diff --git a/assets/themes/src/vscode/palenight/palenight-operator.json b/assets/themes/src/vscode/palenight/palenight-operator.json index 635a2ff7607c455cef191222fc5f13fb8c63024b..450d36cb9ae1233086847429ec795d5ff8e41a9f 100644 --- a/assets/themes/src/vscode/palenight/palenight-operator.json +++ b/assets/themes/src/vscode/palenight/palenight-operator.json @@ -797,14 +797,14 @@ } }, { - "name": "CoffeeScript Variable Assignment", + "name": "CoffeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#89DDFF" } }, { - "name": "CoffeeScript Parameter Function", + "name": "CoffeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#bfc7d5" @@ -1523,14 +1523,14 @@ } }, { - "name": "handlebars entity attribute names", + "name": "handlebars enitity attribute names", "scope": "entity.other.attribute-name.handlebars", "settings": { "foreground": "#89DDFF" } }, { - "name": "handlebars entity attribute values", + "name": "handlebars enitity attribute values", "scope": "entity.other.attribute-value.handlebars variable.parameter.handlebars", "settings": { "foreground": "#7986E7" @@ -1558,7 +1558,7 @@ "keyword.operator.expression.in", "keyword.operator.type", "punctuation.section.embedded.js", - "punctuation.definition.string", + "punctuation.definintion.string", "punctuation" ], "settings": { diff --git a/assets/themes/src/vscode/palenight/palenight.json b/assets/themes/src/vscode/palenight/palenight.json index 5cf68749f42ad9d0b81cfaf06cb8639cb8d3e717..cfbf2f8788c13cc66abfeccf9b0d619416fb642b 100644 --- a/assets/themes/src/vscode/palenight/palenight.json +++ b/assets/themes/src/vscode/palenight/palenight.json @@ -797,14 +797,14 @@ } }, { - "name": "CoffeeScript Variable Assignment", + "name": "CoffeScript Variable Assignment", "scope": "variable.assignment.coffee", "settings": { "foreground": "#89DDFF" } }, { - "name": "CoffeeScript Parameter Function", + "name": "CoffeScript Parameter Function", "scope": "variable.parameter.function.coffee", "settings": { "foreground": "#bfc7d5" @@ -1523,14 +1523,14 @@ } }, { - "name": "handlebars entity attribute names", + "name": "handlebars enitity attribute names", "scope": "entity.other.attribute-name.handlebars", "settings": { "foreground": "#89DDFF" } }, { - "name": "handlebars entity attribute values", + "name": "handlebars enitity attribute values", "scope": "entity.other.attribute-value.handlebars variable.parameter.handlebars", "settings": { "foreground": "#7986E7" @@ -1558,7 +1558,7 @@ "keyword.operator.expression.in", "keyword.operator.type", "punctuation.section.embedded.js", - "punctuation.definition.string", + "punctuation.definintion.string", "punctuation" ], "settings": { From 9521f491603059a2ac4e8efee4aa4dc479e09938 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 17 Jan 2024 19:06:19 -0500 Subject: [PATCH 65/96] Clean up references in doc comments in `lsp` crate (#4109) This PR cleans up a handful of references in doc comments in the `lsp` crate so that `rustdoc` will link and display them correctly. Release Notes: - N/A --- crates/lsp/src/lsp.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 0c1574f5aaff21253c1d1eda695939c801283f2e..9b9aa55ed2d5ef572afb0d912481ecad23474e3b 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -584,7 +584,7 @@ impl LanguageServer { Ok(Arc::new(self)) } - /// Sends a shutdown request to the language server process and prepares the `LanguageServer` to be dropped. + /// Sends a shutdown request to the language server process and prepares the [`LanguageServer`] to be dropped. pub fn shutdown(&self) -> Option>> { if let Some(tasks) = self.io_tasks.lock().take() { let response_handlers = self.response_handlers.clone(); @@ -645,7 +645,7 @@ impl LanguageServer { self.on_custom_request(T::METHOD, f) } - /// Register a handler to inspect all language server process stdio. + /// Registers a handler to inspect all language server process stdio. #[must_use] pub fn on_io(&self, f: F) -> Subscription where @@ -659,17 +659,17 @@ impl LanguageServer { } } - /// Removes a request handler registers via [Self::on_request]. + /// Removes a request handler registers via [`Self::on_request`]. pub fn remove_request_handler(&self) { self.notification_handlers.lock().remove(T::METHOD); } - /// Removes a notification handler registers via [Self::on_notification]. + /// Removes a notification handler registers via [`Self::on_notification`]. pub fn remove_notification_handler(&self) { self.notification_handlers.lock().remove(T::METHOD); } - /// Checks if a notification handler has been registered via [Self::on_notification]. + /// Checks if a notification handler has been registered via [`Self::on_notification`]. pub fn has_notification_handler(&self) -> bool { self.notification_handlers.lock().contains_key(T::METHOD) } @@ -1055,12 +1055,12 @@ impl LanguageServer { #[cfg(any(test, feature = "test-support"))] impl FakeLanguageServer { - /// See [LanguageServer::notify] + /// See [`LanguageServer::notify`]. pub fn notify(&self, params: T::Params) { self.server.notify::(params).ok(); } - /// See [LanguageServer::request] + /// See [`LanguageServer::request`]. pub async fn request(&self, params: T::Params) -> Result where T: request::Request, @@ -1070,7 +1070,7 @@ impl FakeLanguageServer { self.server.request::(params).await } - /// Attempts [try_receive_notification], unwrapping if it has not received the specified type yet. + /// Attempts [`Self::try_receive_notification`], unwrapping if it has not received the specified type yet. pub async fn receive_notification(&mut self) -> T::Params { self.server.executor.start_waiting(); self.try_receive_notification::().await.unwrap() From edb204511c0339de1726038bcad4a8a046bf252e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 16:24:33 -0800 Subject: [PATCH 66/96] Fix selection bug in editor causing selections to be un-ended --- crates/editor/src/completions.rs | 28 --------------- crates/editor/src/element.rs | 32 ++++++++--------- .../src/platform/mac/window_appearance.rs | 35 ------------------- 3 files changed, 15 insertions(+), 80 deletions(-) delete mode 100644 crates/editor/src/completions.rs delete mode 100644 crates/gpui/src/platform/mac/window_appearance.rs diff --git a/crates/editor/src/completions.rs b/crates/editor/src/completions.rs deleted file mode 100644 index b9461364af44e686fc7207d2dd1c800a49156394..0000000000000000000000000000000000000000 --- a/crates/editor/src/completions.rs +++ /dev/null @@ -1,28 +0,0 @@ -use futures::Future; -use gpui::Task; -use smallvec::{smallvec, SmallVec}; -use text::Anchor; - -use crate::Editor; - -struct Completions { - trigger_characters: SmallVec<[char; 1]>, - language: Option, - provider: Box Option>>, -} - -impl Completions { - fn new(f: impl Fn(&mut Editor, &Anchor, &str) -> Option> + 'static) -> Self { - Self { - trigger_characters: smallvec![], - language: None, - provider: Box::new(f), - } - } -} - -impl Editor { - /// Provide completions to the editor when the given character is typed - /// - fn provide_completions(config: Completions) {} -} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 518e03c27ee1e3670c7b53df213bf9200dd56998..d7ce842e1d7de4e2a278beaa3ff73ec24bc5405f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -456,6 +456,7 @@ impl EditorElement { event: &MouseUpEvent, position_map: &PositionMap, text_bounds: Bounds, + interactive_bounds: &InteractiveBounds, stacking_order: &StackingOrder, cx: &mut ViewContext, ) { @@ -466,7 +467,8 @@ impl EditorElement { editor.select(SelectPhase::End, cx); } - if !pending_nonempty_selections + if interactive_bounds.visibly_contains(&event.position, cx) + && !pending_nonempty_selections && event.modifiers.command && text_bounds.contains(&event.position) && cx.was_top_layer(&event.position, stacking_order) @@ -2542,22 +2544,18 @@ impl EditorElement { let interactive_bounds = interactive_bounds.clone(); move |event: &MouseUpEvent, phase, cx| { - if phase == DispatchPhase::Bubble - { - // if interactive_bounds.visibly_contains(&event.position, cx) { - editor.update(cx, |editor, cx| { - Self::mouse_up( - editor, - event, - &position_map, - text_bounds, - &stacking_order, - cx, - ) - }); - // } else { - - // } + if phase == DispatchPhase::Bubble { + editor.update(cx, |editor, cx| { + Self::mouse_up( + editor, + event, + &position_map, + text_bounds, + &interactive_bounds, + &stacking_order, + cx, + ) + }); } } }); diff --git a/crates/gpui/src/platform/mac/window_appearance.rs b/crates/gpui/src/platform/mac/window_appearance.rs deleted file mode 100644 index 2edc896289ef8056424a0399d38ff937155adad2..0000000000000000000000000000000000000000 --- a/crates/gpui/src/platform/mac/window_appearance.rs +++ /dev/null @@ -1,35 +0,0 @@ -use crate::WindowAppearance; -use cocoa::{ - appkit::{NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight}, - base::id, - foundation::NSString, -}; -use objc::{msg_send, sel, sel_impl}; -use std::ffi::CStr; - -impl WindowAppearance { - pub unsafe fn from_native(appearance: id) -> Self { - let name: id = msg_send![appearance, name]; - if name == NSAppearanceNameVibrantLight { - Self::VibrantLight - } else if name == NSAppearanceNameVibrantDark { - Self::VibrantDark - } else if name == NSAppearanceNameAqua { - Self::Light - } else if name == NSAppearanceNameDarkAqua { - Self::Dark - } else { - println!( - "unknown appearance: {:?}", - CStr::from_ptr(name.UTF8String()) - ); - Self::Light - } - } -} - -#[link(name = "AppKit", kind = "framework")] -extern "C" { - pub static NSAppearanceNameAqua: id; - pub static NSAppearanceNameDarkAqua: id; -} From 4070eefa4916478bcd2121feb42aad8a36edb9cd Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 16:55:54 -0800 Subject: [PATCH 67/96] Use cargo to install typo check --- .github/workflows/ci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b9ca8de8e015de0161a1be1c8fddb1f3fa9d825..bf19e1c53a21afc7891e47080fbd84f3912636af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,8 +38,16 @@ jobs: - name: Set up default .cargo/config.toml run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml + - name: Check spelling + run: | + if ! command -v typos > /dev/null; then + cargo install typos-cli + fi + typos + - name: Run style checks uses: ./.github/actions/check_style + tests: name: Run tests runs-on: From 6ea23d0704cd45b4719977ec1c2679c33be038d3 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 17:03:17 -0800 Subject: [PATCH 68/96] Fix canary --- crates/gpui/src/gpui.rs | 1 - typos.toml | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 1c7e7432889b2a20ba2c3bfaf850581c8c323a5c..6f5e30149d9691b3c364d62ab2e3ce6ec7da1b4c 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -88,7 +88,6 @@ use std::{ }; use taffy::TaffyLayoutEngine; -/// Here's a spelling mistake: visibile pub trait Context { type Result; diff --git a/typos.toml b/typos.toml index 2881b65f4844bdbfb3e992d97afe54adbf2c3e7e..09625dbcf9786621cdc73b22baa151fbaf2edd77 100644 --- a/typos.toml +++ b/typos.toml @@ -1,14 +1,15 @@ [files] ignore-files = true extend-exclude = [ - # Vim makes heavy use of partial typing tables - "crates/vim/*", # glsl isn't recognized by this tool "crates/zed/src/languages/glsl/*", # File suffixes aren't typos "assets/icons/file_icons/file_types.json", # Not our typos "assets/themes/src/vscode/*", + "crates/live_kit_server/*" + # Vim makes heavy use of partial typing tables + "crates/vim/*", # Editor and file finder rely on partial typing and custom in-string syntax "crates/file_finder/src/file_finder.rs", "crates/editor/src/editor_tests.rs", From 58333b96dd6706dcc70e9ea46f3bc6ebc6f2bf67 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 17 Jan 2024 17:04:00 -0800 Subject: [PATCH 69/96] Adjust config --- .github/actions/check_style/action.yml | 3 --- .github/workflows/ci.yml | 2 +- typos.toml | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/actions/check_style/action.yml b/.github/actions/check_style/action.yml index 290496d7e7866490286fafeb31d867b73c20ab80..25020e4e4c5399026c1ab32622903a3779ba86b2 100644 --- a/.github/actions/check_style/action.yml +++ b/.github/actions/check_style/action.yml @@ -21,6 +21,3 @@ runs: run: | export SQUAWK_GITHUB_TOKEN=${{ github.token }} . ./script/squawk - - - name: Run spelling check - uses: crate-ci/typos@master diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf19e1c53a21afc7891e47080fbd84f3912636af..2c660b7a0d3883b0bac1e45bdce56a85fd0e6108 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: - name: Check spelling run: | - if ! command -v typos > /dev/null; then + if ! which typos > /dev/null; then cargo install typos-cli fi typos diff --git a/typos.toml b/typos.toml index 09625dbcf9786621cdc73b22baa151fbaf2edd77..115cc14478cb94feb8320fff5b7b67b555864a6d 100644 --- a/typos.toml +++ b/typos.toml @@ -7,7 +7,7 @@ extend-exclude = [ "assets/icons/file_icons/file_types.json", # Not our typos "assets/themes/src/vscode/*", - "crates/live_kit_server/*" + "crates/live_kit_server/*", # Vim makes heavy use of partial typing tables "crates/vim/*", # Editor and file finder rely on partial typing and custom in-string syntax From ab1bea515c41a7e35d8ffdcca3175bca186a62b3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Jan 2024 15:46:36 -0800 Subject: [PATCH 70/96] Store the impersonator id on access tokens created via ZED_IMPERSONATE * Use the impersonator id to prevent these tokens from counting against the impersonated user when limiting the users' total of access tokens. * When connecting using an access token with an impersonator add the impersonator as a field to the tracing span that wraps the task for that connection. * Disallow impersonating users via the admin API token in production, because when using the admin API token, we aren't able to identify the impersonator. Co-authored-by: Marshall --- .../20221109000000_test_schema.sql | 2 + ...0300_add_impersonator_to_access_tokens.sql | 3 + crates/collab/src/api.rs | 4 +- crates/collab/src/auth.rs | 90 +++++++++++---- crates/collab/src/db/queries/access_tokens.rs | 12 +- crates/collab/src/db/tables/access_token.rs | 1 + crates/collab/src/db/tests/db_tests.rs | 104 ++++++++++++++++-- crates/collab/src/rpc.rs | 20 +++- crates/collab/src/tests/test_server.rs | 1 + 9 files changed, 198 insertions(+), 39 deletions(-) create mode 100644 crates/collab/migrations/20240117150300_add_impersonator_to_access_tokens.sql diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 507cf197f70b9bcce2c74a16b1c6ead569965a5d..a7c9331506732d2c87c72849ea7fea7bd4867726 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -19,9 +19,11 @@ CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"); CREATE TABLE "access_tokens" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "user_id" INTEGER REFERENCES users (id), + "impersonator_id" INTEGER REFERENCES users (id), "hash" VARCHAR(128) ); CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id"); +CREATE INDEX "index_access_tokens_impersonator_id" ON "access_tokens" ("impersonator_id"); CREATE TABLE "contacts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/crates/collab/migrations/20240117150300_add_impersonator_to_access_tokens.sql b/crates/collab/migrations/20240117150300_add_impersonator_to_access_tokens.sql new file mode 100644 index 0000000000000000000000000000000000000000..199706473fd383c84845dcd085a7012bc5fd9919 --- /dev/null +++ b/crates/collab/migrations/20240117150300_add_impersonator_to_access_tokens.sql @@ -0,0 +1,3 @@ +ALTER TABLE access_tokens ADD COLUMN impersonator_id integer; + +CREATE INDEX "index_access_tokens_impersonator_id" ON "access_tokens" ("impersonator_id"); diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index a28aeac9ab23dd293fbfbd9a7c851709855408d4..24a8f066b256c61d1a4fc46375a50cd8c92676cd 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -157,9 +157,11 @@ async fn create_access_token( .ok_or_else(|| anyhow!("user not found"))?; let mut user_id = user.id; + let mut impersonator_id = None; if let Some(impersonate) = params.impersonate { if user.admin { if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? { + impersonator_id = Some(user_id); user_id = impersonated_user.id; } else { return Err(Error::Http( @@ -175,7 +177,7 @@ async fn create_access_token( } } - let access_token = auth::create_access_token(app.db.as_ref(), user_id).await?; + let access_token = auth::create_access_token(app.db.as_ref(), user_id, impersonator_id).await?; let encrypted_access_token = auth::encrypt_access_token(&access_token, params.public_key.clone())?; diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index df3ded28e4a2f1f86ac1421e91dc6be31f7f8408..a32f35fae896781e6014148bd0ccf8a1e1239c40 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -27,6 +27,9 @@ lazy_static! { .unwrap(); } +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Impersonator(pub Option); + /// Validates the authorization header. This has two mechanisms, one for the ADMIN_TOKEN /// and one for the access tokens that we issue. pub async fn validate_header(mut req: Request, next: Next) -> impl IntoResponse { @@ -57,28 +60,50 @@ pub async fn validate_header(mut req: Request, next: Next) -> impl Into })?; let state = req.extensions().get::>().unwrap(); - let credentials_valid = if let Some(admin_token) = access_token.strip_prefix("ADMIN_TOKEN:") { - state.config.api_token == admin_token + + // In development, allow impersonation using the admin API token. + // Don't allow this in production because we can't tell who is doing + // the impersonating. + let validate_result = if let (Some(admin_token), true) = ( + access_token.strip_prefix("ADMIN_TOKEN:"), + state.config.is_development(), + ) { + Ok(VerifyAccessTokenResult { + is_valid: state.config.api_token == admin_token, + impersonator_id: None, + }) } else { - verify_access_token(&access_token, user_id, &state.db) - .await - .unwrap_or(false) + verify_access_token(&access_token, user_id, &state.db).await }; - if credentials_valid { - let user = state - .db - .get_user_by_id(user_id) - .await? - .ok_or_else(|| anyhow!("user {} not found", user_id))?; - req.extensions_mut().insert(user); - Ok::<_, Error>(next.run(req).await) - } else { - Err(Error::Http( - StatusCode::UNAUTHORIZED, - "invalid credentials".to_string(), - )) + if let Ok(validate_result) = validate_result { + if validate_result.is_valid { + let user = state + .db + .get_user_by_id(user_id) + .await? + .ok_or_else(|| anyhow!("user {} not found", user_id))?; + + let impersonator = if let Some(impersonator_id) = validate_result.impersonator_id { + let impersonator = state + .db + .get_user_by_id(impersonator_id) + .await? + .ok_or_else(|| anyhow!("user {} not found", impersonator_id))?; + Some(impersonator) + } else { + None + }; + req.extensions_mut().insert(user); + req.extensions_mut().insert(Impersonator(impersonator)); + return Ok::<_, Error>(next.run(req).await); + } } + + Err(Error::Http( + StatusCode::UNAUTHORIZED, + "invalid credentials".to_string(), + )) } const MAX_ACCESS_TOKENS_TO_STORE: usize = 8; @@ -92,13 +117,22 @@ struct AccessTokenJson { /// Creates a new access token to identify the given user. before returning it, you should /// encrypt it with the user's public key. -pub async fn create_access_token(db: &db::Database, user_id: UserId) -> Result { +pub async fn create_access_token( + db: &db::Database, + user_id: UserId, + impersonator_id: Option, +) -> Result { const VERSION: usize = 1; let access_token = rpc::auth::random_token(); let access_token_hash = hash_access_token(&access_token).context("failed to hash access token")?; let id = db - .create_access_token(user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE) + .create_access_token( + user_id, + impersonator_id, + &access_token_hash, + MAX_ACCESS_TOKENS_TO_STORE, + ) .await?; Ok(serde_json::to_string(&AccessTokenJson { version: VERSION, @@ -137,8 +171,17 @@ pub fn encrypt_access_token(access_token: &str, public_key: String) -> Result, +} + /// verify access token returns true if the given token is valid for the given user. -pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc) -> Result { +pub async fn verify_access_token( + token: &str, + user_id: UserId, + db: &Arc, +) -> Result { let token: AccessTokenJson = serde_json::from_str(&token)?; let db_token = db.get_access_token(token.id).await?; @@ -154,5 +197,8 @@ pub async fn verify_access_token(token: &str, user_id: UserId, db: &Arc, access_token_hash: &str, max_access_token_count: usize, ) -> Result { @@ -14,19 +15,28 @@ impl Database { let token = access_token::ActiveModel { user_id: ActiveValue::set(user_id), + impersonator_id: ActiveValue::set(impersonator_id), hash: ActiveValue::set(access_token_hash.into()), ..Default::default() } .insert(&*tx) .await?; + let existing_token_filter = if let Some(impersonator_id) = impersonator_id { + access_token::Column::ImpersonatorId.eq(impersonator_id) + } else { + access_token::Column::UserId + .eq(user_id) + .and(access_token::Column::ImpersonatorId.is_null()) + }; + access_token::Entity::delete_many() .filter( access_token::Column::Id.in_subquery( Query::select() .column(access_token::Column::Id) .from(access_token::Entity) - .and_where(access_token::Column::UserId.eq(user_id)) + .cond_where(existing_token_filter) .order_by(access_token::Column::Id, sea_orm::Order::Desc) .limit(10000) .offset(max_access_token_count as u64) diff --git a/crates/collab/src/db/tables/access_token.rs b/crates/collab/src/db/tables/access_token.rs index da7392b98c444f3d83fd549525f9af4a2eb125d3..81d6f3af6020e55112e7bf8555f9dac53cfa8304 100644 --- a/crates/collab/src/db/tables/access_token.rs +++ b/crates/collab/src/db/tables/access_token.rs @@ -7,6 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: AccessTokenId, pub user_id: UserId, + pub impersonator_id: Option, pub hash: String, } diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 5332f227ef4277ada2fce222bb7097ef0da396b3..98c35aa6467e0fcf7d821da8b018946cbe1902b8 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -146,7 +146,7 @@ test_both_dbs!( ); async fn test_create_access_tokens(db: &Arc) { - let user = db + let user_1 = db .create_user( "u1@example.com", false, @@ -158,14 +158,27 @@ async fn test_create_access_tokens(db: &Arc) { .await .unwrap() .user_id; + let user_2 = db + .create_user( + "u2@example.com", + false, + NewUserParams { + github_login: "u2".into(), + github_user_id: 2, + }, + ) + .await + .unwrap() + .user_id; - let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); - let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); + let token_1 = db.create_access_token(user_1, None, "h1", 2).await.unwrap(); + let token_2 = db.create_access_token(user_1, None, "h2", 2).await.unwrap(); assert_eq!( db.get_access_token(token_1).await.unwrap(), access_token::Model { id: token_1, - user_id: user, + user_id: user_1, + impersonator_id: None, hash: "h1".into(), } ); @@ -173,17 +186,19 @@ async fn test_create_access_tokens(db: &Arc) { db.get_access_token(token_2).await.unwrap(), access_token::Model { id: token_2, - user_id: user, + user_id: user_1, + impersonator_id: None, hash: "h2".into() } ); - let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); + let token_3 = db.create_access_token(user_1, None, "h3", 2).await.unwrap(); assert_eq!( db.get_access_token(token_3).await.unwrap(), access_token::Model { id: token_3, - user_id: user, + user_id: user_1, + impersonator_id: None, hash: "h3".into() } ); @@ -191,18 +206,20 @@ async fn test_create_access_tokens(db: &Arc) { db.get_access_token(token_2).await.unwrap(), access_token::Model { id: token_2, - user_id: user, + user_id: user_1, + impersonator_id: None, hash: "h2".into() } ); assert!(db.get_access_token(token_1).await.is_err()); - let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); + let token_4 = db.create_access_token(user_1, None, "h4", 2).await.unwrap(); assert_eq!( db.get_access_token(token_4).await.unwrap(), access_token::Model { id: token_4, - user_id: user, + user_id: user_1, + impersonator_id: None, hash: "h4".into() } ); @@ -210,12 +227,77 @@ async fn test_create_access_tokens(db: &Arc) { db.get_access_token(token_3).await.unwrap(), access_token::Model { id: token_3, - user_id: user, + user_id: user_1, + impersonator_id: None, hash: "h3".into() } ); assert!(db.get_access_token(token_2).await.is_err()); assert!(db.get_access_token(token_1).await.is_err()); + + // An access token for user 2 impersonating user 1 does not + // count against user 1's access token limit (of 2). + let token_5 = db + .create_access_token(user_1, Some(user_2), "h5", 2) + .await + .unwrap(); + assert_eq!( + db.get_access_token(token_5).await.unwrap(), + access_token::Model { + id: token_5, + user_id: user_1, + impersonator_id: Some(user_2), + hash: "h5".into() + } + ); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user_1, + impersonator_id: None, + hash: "h3".into() + } + ); + + // Only a limited number (2) of access tokens are stored for user 2 + // impersonating other users. + let token_6 = db + .create_access_token(user_1, Some(user_2), "h6", 2) + .await + .unwrap(); + let token_7 = db + .create_access_token(user_1, Some(user_2), "h7", 2) + .await + .unwrap(); + assert_eq!( + db.get_access_token(token_6).await.unwrap(), + access_token::Model { + id: token_6, + user_id: user_1, + impersonator_id: Some(user_2), + hash: "h6".into() + } + ); + assert_eq!( + db.get_access_token(token_7).await.unwrap(), + access_token::Model { + id: token_7, + user_id: user_1, + impersonator_id: Some(user_2), + hash: "h7".into() + } + ); + assert!(db.get_access_token(token_5).await.is_err()); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user_1, + impersonator_id: None, + hash: "h3".into() + } + ); } test_both_dbs!( diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 9406b4938a8f2795a89cd8f10238964710eb61f3..c7bbf7f865096cedbde8e1b5bf2e23338d01a824 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1,7 +1,7 @@ mod connection_pool; use crate::{ - auth, + auth::{self, Impersonator}, db::{ self, BufferId, ChannelId, ChannelRole, ChannelsForUser, CreateChannelResult, CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId, @@ -65,7 +65,7 @@ use std::{ use time::OffsetDateTime; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; -use tracing::{info_span, instrument, Instrument}; +use tracing::{field, info_span, instrument, Instrument}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); @@ -561,13 +561,17 @@ impl Server { connection: Connection, address: String, user: User, + impersonator: Option, mut send_connection_id: Option>, executor: Executor, ) -> impl Future> { let this = self.clone(); let user_id = user.id; let login = user.github_login; - let span = info_span!("handle connection", %user_id, %login, %address); + let span = info_span!("handle connection", %user_id, %login, %address, impersonator = field::Empty); + if let Some(impersonator) = impersonator { + span.record("impersonator", &impersonator.github_login); + } let mut teardown = self.teardown.subscribe(); async move { let (connection_id, handle_io, mut incoming_rx) = this @@ -839,6 +843,7 @@ pub async fn handle_websocket_request( ConnectInfo(socket_address): ConnectInfo, Extension(server): Extension>, Extension(user): Extension, + Extension(impersonator): Extension, ws: WebSocketUpgrade, ) -> axum::response::Response { if protocol_version != rpc::PROTOCOL_VERSION { @@ -858,7 +863,14 @@ pub async fn handle_websocket_request( let connection = Connection::new(Box::pin(socket)); async move { server - .handle_connection(connection, socket_address, user, None, Executor::Production) + .handle_connection( + connection, + socket_address, + user, + impersonator.0, + None, + Executor::Production, + ) .await .log_err(); } diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index cda0621cb32385a399fdfdaef51821dd531281b2..ea08d83b6cbe71c4516c1c8bab4d46edce6cf60d 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -213,6 +213,7 @@ impl TestServer { server_conn, client_name, user, + None, Some(connection_id_tx), Executor::Deterministic(cx.background_executor().clone()), )) From 69bff7bb77dca9916c07e7890a5abbabdd2d4dff Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Jan 2024 17:35:37 -0800 Subject: [PATCH 71/96] Exclude squawk rule forbidding regular-sized integers --- script/lib/squawk.toml | 4 ++++ script/squawk | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 script/lib/squawk.toml diff --git a/script/lib/squawk.toml b/script/lib/squawk.toml new file mode 100644 index 0000000000000000000000000000000000000000..1090e382863a8950de3da11b9ca34e08f2014e45 --- /dev/null +++ b/script/lib/squawk.toml @@ -0,0 +1,4 @@ +excluded_rules = [ + "prefer-big-int", + "prefer-bigint-over-int", +] diff --git a/script/squawk b/script/squawk index 0fb3e5a3325e8005fe4f0667debc7626cab5c9bd..68977645d06a5d209ccfba757b6cbe427372e039 100755 --- a/script/squawk +++ b/script/squawk @@ -8,13 +8,12 @@ set -e if [ -z "$GITHUB_BASE_REF" ]; then echo 'Not a pull request, skipping squawk modified migrations linting' - return 0 + exit fi SQUAWK_VERSION=0.26.0 SQUAWK_BIN="./target/squawk-$SQUAWK_VERSION" -SQUAWK_ARGS="--assume-in-transaction" - +SQUAWK_ARGS="--assume-in-transaction --config script/lib/squawk.toml" if [ ! -f "$SQUAWK_BIN" ]; then curl -L -o "$SQUAWK_BIN" "https://github.com/sbdchd/squawk/releases/download/v$SQUAWK_VERSION/squawk-darwin-x86_64" From 9f04fd9019018849655cfe4a1cdb983094a316f6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Jan 2024 17:58:59 -0800 Subject: [PATCH 72/96] For impersonating access tokens, store impersonatee in the new column This way, we don't need an index on both columns --- .../20221109000000_test_schema.sql | 3 +- ...0300_add_impersonator_to_access_tokens.sql | 4 +-- crates/collab/src/api.rs | 11 +++--- crates/collab/src/auth.rs | 13 ++++--- crates/collab/src/db/queries/access_tokens.rs | 14 ++------ crates/collab/src/db/tables/access_token.rs | 2 +- crates/collab/src/db/tests/db_tests.rs | 34 +++++++++---------- docs/src/developing_zed__building_zed.md | 2 +- 8 files changed, 38 insertions(+), 45 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index a7c9331506732d2c87c72849ea7fea7bd4867726..8d8f523c94c2caa2e70212f3e08ae6f4f493599a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -19,11 +19,10 @@ CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id"); CREATE TABLE "access_tokens" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "user_id" INTEGER REFERENCES users (id), - "impersonator_id" INTEGER REFERENCES users (id), + "impersonated_user_id" INTEGER REFERENCES users (id), "hash" VARCHAR(128) ); CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id"); -CREATE INDEX "index_access_tokens_impersonator_id" ON "access_tokens" ("impersonator_id"); CREATE TABLE "contacts" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/crates/collab/migrations/20240117150300_add_impersonator_to_access_tokens.sql b/crates/collab/migrations/20240117150300_add_impersonator_to_access_tokens.sql index 199706473fd383c84845dcd085a7012bc5fd9919..8c79640cd88bfad58e5f9eafda90ae2d80e4e834 100644 --- a/crates/collab/migrations/20240117150300_add_impersonator_to_access_tokens.sql +++ b/crates/collab/migrations/20240117150300_add_impersonator_to_access_tokens.sql @@ -1,3 +1 @@ -ALTER TABLE access_tokens ADD COLUMN impersonator_id integer; - -CREATE INDEX "index_access_tokens_impersonator_id" ON "access_tokens" ("impersonator_id"); +ALTER TABLE access_tokens ADD COLUMN impersonated_user_id integer; diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 24a8f066b256c61d1a4fc46375a50cd8c92676cd..6bdbd7357fb857c4db90bfc0f5583023d3b76daf 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -156,13 +156,11 @@ async fn create_access_token( .await? .ok_or_else(|| anyhow!("user not found"))?; - let mut user_id = user.id; - let mut impersonator_id = None; + let mut impersonated_user_id = None; if let Some(impersonate) = params.impersonate { if user.admin { if let Some(impersonated_user) = app.db.get_user_by_github_login(&impersonate).await? { - impersonator_id = Some(user_id); - user_id = impersonated_user.id; + impersonated_user_id = Some(impersonated_user.id); } else { return Err(Error::Http( StatusCode::UNPROCESSABLE_ENTITY, @@ -177,12 +175,13 @@ async fn create_access_token( } } - let access_token = auth::create_access_token(app.db.as_ref(), user_id, impersonator_id).await?; + let access_token = + auth::create_access_token(app.db.as_ref(), user_id, impersonated_user_id).await?; let encrypted_access_token = auth::encrypt_access_token(&access_token, params.public_key.clone())?; Ok(Json(CreateAccessTokenResponse { - user_id, + user_id: impersonated_user_id.unwrap_or(user_id), encrypted_access_token, })) } diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index a32f35fae896781e6014148bd0ccf8a1e1239c40..e6c43df73c6dc9972849acc5ef5fff704553f77a 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -120,7 +120,7 @@ struct AccessTokenJson { pub async fn create_access_token( db: &db::Database, user_id: UserId, - impersonator_id: Option, + impersonated_user_id: Option, ) -> Result { const VERSION: usize = 1; let access_token = rpc::auth::random_token(); @@ -129,7 +129,7 @@ pub async fn create_access_token( let id = db .create_access_token( user_id, - impersonator_id, + impersonated_user_id, &access_token_hash, MAX_ACCESS_TOKENS_TO_STORE, ) @@ -185,7 +185,8 @@ pub async fn verify_access_token( let token: AccessTokenJson = serde_json::from_str(&token)?; let db_token = db.get_access_token(token.id).await?; - if db_token.user_id != user_id { + let token_user_id = db_token.impersonated_user_id.unwrap_or(db_token.user_id); + if token_user_id != user_id { return Err(anyhow!("no such access token"))?; } @@ -199,6 +200,10 @@ pub async fn verify_access_token( METRIC_ACCESS_TOKEN_HASHING_TIME.observe(duration.as_millis() as f64); Ok(VerifyAccessTokenResult { is_valid, - impersonator_id: db_token.impersonator_id, + impersonator_id: if db_token.impersonated_user_id.is_some() { + Some(db_token.user_id) + } else { + None + }, }) } diff --git a/crates/collab/src/db/queries/access_tokens.rs b/crates/collab/src/db/queries/access_tokens.rs index e0db6c5038dad69230a0a57fc5401b5117937312..af58d51a3343fd84acb604b01f5030260b558376 100644 --- a/crates/collab/src/db/queries/access_tokens.rs +++ b/crates/collab/src/db/queries/access_tokens.rs @@ -6,7 +6,7 @@ impl Database { pub async fn create_access_token( &self, user_id: UserId, - impersonator_id: Option, + impersonated_user_id: Option, access_token_hash: &str, max_access_token_count: usize, ) -> Result { @@ -15,28 +15,20 @@ impl Database { let token = access_token::ActiveModel { user_id: ActiveValue::set(user_id), - impersonator_id: ActiveValue::set(impersonator_id), + impersonated_user_id: ActiveValue::set(impersonated_user_id), hash: ActiveValue::set(access_token_hash.into()), ..Default::default() } .insert(&*tx) .await?; - let existing_token_filter = if let Some(impersonator_id) = impersonator_id { - access_token::Column::ImpersonatorId.eq(impersonator_id) - } else { - access_token::Column::UserId - .eq(user_id) - .and(access_token::Column::ImpersonatorId.is_null()) - }; - access_token::Entity::delete_many() .filter( access_token::Column::Id.in_subquery( Query::select() .column(access_token::Column::Id) .from(access_token::Entity) - .cond_where(existing_token_filter) + .and_where(access_token::Column::UserId.eq(user_id)) .order_by(access_token::Column::Id, sea_orm::Order::Desc) .limit(10000) .offset(max_access_token_count as u64) diff --git a/crates/collab/src/db/tables/access_token.rs b/crates/collab/src/db/tables/access_token.rs index 81d6f3af6020e55112e7bf8555f9dac53cfa8304..22635fb64d94538687eac590efaf75049c64c864 100644 --- a/crates/collab/src/db/tables/access_token.rs +++ b/crates/collab/src/db/tables/access_token.rs @@ -7,7 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: AccessTokenId, pub user_id: UserId, - pub impersonator_id: Option, + pub impersonated_user_id: Option, pub hash: String, } diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 98c35aa6467e0fcf7d821da8b018946cbe1902b8..3e1bdede71e3691dc1da0e0df0fb59c6c92ac83b 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -178,7 +178,7 @@ async fn test_create_access_tokens(db: &Arc) { access_token::Model { id: token_1, user_id: user_1, - impersonator_id: None, + impersonated_user_id: None, hash: "h1".into(), } ); @@ -187,7 +187,7 @@ async fn test_create_access_tokens(db: &Arc) { access_token::Model { id: token_2, user_id: user_1, - impersonator_id: None, + impersonated_user_id: None, hash: "h2".into() } ); @@ -198,7 +198,7 @@ async fn test_create_access_tokens(db: &Arc) { access_token::Model { id: token_3, user_id: user_1, - impersonator_id: None, + impersonated_user_id: None, hash: "h3".into() } ); @@ -207,7 +207,7 @@ async fn test_create_access_tokens(db: &Arc) { access_token::Model { id: token_2, user_id: user_1, - impersonator_id: None, + impersonated_user_id: None, hash: "h2".into() } ); @@ -219,7 +219,7 @@ async fn test_create_access_tokens(db: &Arc) { access_token::Model { id: token_4, user_id: user_1, - impersonator_id: None, + impersonated_user_id: None, hash: "h4".into() } ); @@ -228,7 +228,7 @@ async fn test_create_access_tokens(db: &Arc) { access_token::Model { id: token_3, user_id: user_1, - impersonator_id: None, + impersonated_user_id: None, hash: "h3".into() } ); @@ -238,15 +238,15 @@ async fn test_create_access_tokens(db: &Arc) { // An access token for user 2 impersonating user 1 does not // count against user 1's access token limit (of 2). let token_5 = db - .create_access_token(user_1, Some(user_2), "h5", 2) + .create_access_token(user_2, Some(user_1), "h5", 2) .await .unwrap(); assert_eq!( db.get_access_token(token_5).await.unwrap(), access_token::Model { id: token_5, - user_id: user_1, - impersonator_id: Some(user_2), + user_id: user_2, + impersonated_user_id: Some(user_1), hash: "h5".into() } ); @@ -255,7 +255,7 @@ async fn test_create_access_tokens(db: &Arc) { access_token::Model { id: token_3, user_id: user_1, - impersonator_id: None, + impersonated_user_id: None, hash: "h3".into() } ); @@ -263,19 +263,19 @@ async fn test_create_access_tokens(db: &Arc) { // Only a limited number (2) of access tokens are stored for user 2 // impersonating other users. let token_6 = db - .create_access_token(user_1, Some(user_2), "h6", 2) + .create_access_token(user_2, Some(user_1), "h6", 2) .await .unwrap(); let token_7 = db - .create_access_token(user_1, Some(user_2), "h7", 2) + .create_access_token(user_2, Some(user_1), "h7", 2) .await .unwrap(); assert_eq!( db.get_access_token(token_6).await.unwrap(), access_token::Model { id: token_6, - user_id: user_1, - impersonator_id: Some(user_2), + user_id: user_2, + impersonated_user_id: Some(user_1), hash: "h6".into() } ); @@ -283,8 +283,8 @@ async fn test_create_access_tokens(db: &Arc) { db.get_access_token(token_7).await.unwrap(), access_token::Model { id: token_7, - user_id: user_1, - impersonator_id: Some(user_2), + user_id: user_2, + impersonated_user_id: Some(user_1), hash: "h7".into() } ); @@ -294,7 +294,7 @@ async fn test_create_access_tokens(db: &Arc) { access_token::Model { id: token_3, user_id: user_1, - impersonator_id: None, + impersonated_user_id: None, hash: "h3".into() } ); diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index 7535ceb4d0193e01b46e1cf1f9e2c818c086f138..a360be83975995a37870900af6bf7166a666c9b6 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -14,7 +14,7 @@ - Ensure that the Xcode command line tools are using your newly installed copy of Xcode: ``` - sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer. + sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer ``` * Install the Rust wasm toolchain: From 93d068a7467cc12ad68d4ab1ca5c0bfd9397aa33 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 17 Jan 2024 18:01:38 -0800 Subject: [PATCH 73/96] Update verify_access_token doc comment --- crates/collab/src/auth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab/src/auth.rs b/crates/collab/src/auth.rs index e6c43df73c6dc9972849acc5ef5fff704553f77a..dc0374df6a764d06282b68cdd4042afcd5875ac8 100644 --- a/crates/collab/src/auth.rs +++ b/crates/collab/src/auth.rs @@ -176,7 +176,7 @@ pub struct VerifyAccessTokenResult { pub impersonator_id: Option, } -/// verify access token returns true if the given token is valid for the given user. +/// Checks that the given access token is valid for the given user. pub async fn verify_access_token( token: &str, user_id: UserId, From 1d3ca8eb5d77e2c37e646ee1322fe32c4eaab06d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 17 Jan 2024 21:18:07 -0700 Subject: [PATCH 74/96] Adjust APIs for simpler examples in blog post --- crates/gpui/src/app.rs | 12 +++++++++--- crates/live_kit_client/examples/test_app.rs | 4 ++-- crates/storybook/src/storybook.rs | 3 +-- crates/zed/src/main.rs | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 8f7345ae16ced7e84cb145de81635b8c805e3fe6..477bea143916688b84571b2da3ad24b9bbec52cf 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -111,14 +111,20 @@ pub struct App(Rc); /// configured, you'll start the app with `App::run`. impl App { /// Builds an app with the given asset source. - pub fn production(asset_source: Arc) -> Self { + pub fn new() -> Self { Self(AppContext::new( current_platform(), - asset_source, + Arc::new(()), http::client(), )) } + /// Assign + pub fn with_assets(self, asset_source: impl AssetSource) -> Self { + self.0.borrow_mut().asset_source = Arc::new(asset_source); + self + } + /// Start the application. The provided callback will be called once the /// app is fully launched. pub fn run(self, on_finish_launching: F) @@ -1167,7 +1173,7 @@ impl Context for AppContext { type Result = T; /// Build an entity that is owned by the application. The given function will be invoked with - /// a `ModelContext` and must return an object representing the entity. A `Model` will be returned + /// a `ModelContext` and must return an object representing the entity. A `Model` handle will be returned, /// which can be used to access the entity in a context. fn new_model( &mut self, diff --git a/crates/live_kit_client/examples/test_app.rs b/crates/live_kit_client/examples/test_app.rs index 9fc8aafd30c283df748796790964dab11151d9af..06f297083066a2e749d7ecc3d10f94d4a2167115 100644 --- a/crates/live_kit_client/examples/test_app.rs +++ b/crates/live_kit_client/examples/test_app.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use futures::StreamExt; use gpui::{actions, KeyBinding, Menu, MenuItem}; @@ -12,7 +12,7 @@ actions!(live_kit_client, [Quit]); fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); - gpui::App::production(Arc::new(())).run(|cx| { + gpui::App::new().run(|cx| { #[cfg(any(test, feature = "test-support"))] println!("USING TEST LIVEKIT"); diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index 7da1d67b307e660963c34297fc1fcfcca0c4222b..1c5ffb494bdb24dc794bce42aacc4a8265244b66 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -60,8 +60,7 @@ fn main() { }); let theme_name = args.theme.unwrap_or("One Dark".to_string()); - let asset_source = Arc::new(Assets); - gpui::App::production(asset_source).run(move |cx| { + gpui::App::new().with_assets(Assets).run(move |cx| { load_embedded_fonts(cx).unwrap(); let mut store = SettingsStore::default(); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 821668001c4fa42f757eea55b5e80361e0456e6d..82f608a87f38b544343c99ac2d54e066c3d08307 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -67,7 +67,7 @@ fn main() { } log::info!("========== starting zed =========="); - let app = App::production(Arc::new(Assets)); + let app = App::new().with_assets(Assets); let (installation_id, existing_installation_id_found) = app .background_executor() From bef1b8326557f843c1888a5edbd90e128389928b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 17 Jan 2024 21:18:17 -0700 Subject: [PATCH 75/96] Add ownership post example --- crates/gpui/examples/ownership_post.rs | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 crates/gpui/examples/ownership_post.rs diff --git a/crates/gpui/examples/ownership_post.rs b/crates/gpui/examples/ownership_post.rs new file mode 100644 index 0000000000000000000000000000000000000000..603b63b254644f808bebb0477d569a58f9dfea70 --- /dev/null +++ b/crates/gpui/examples/ownership_post.rs @@ -0,0 +1,34 @@ +use gpui::{prelude::*, App, AppContext, EventEmitter, Model, ModelContext}; + +struct Counter { + count: usize, +} + +fn main() { + App::new().run(|cx: &mut AppContext| { + let counter: Model = cx.new_model(|_cx| Counter { count: 0 }); + let observer = cx.new_model(|cx: &mut ModelContext| { + cx.observe(&counter, |observer, observed, cx| { + observer.count = observed.read(cx).count * 2; + }) + .detach(); + + Counter { + count: counter.read(cx).count * 2, + } + }); + + counter.update(cx, |counter, cx| { + counter.count += 1; + cx.notify(); + }); + + assert_eq!(observer.read(cx).count, 2); + }); +} + +struct Change { + delta: isize, +} + +impl EventEmitter for Counter {} From b807e6fe8067f1288b771471763d58172ce3004e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 18 Jan 2024 00:58:50 -0500 Subject: [PATCH 76/96] Use try_global() --- crates/auto_update/src/auto_update.rs | 37 ++++++++++--------- crates/client/src/telemetry.rs | 8 ++-- crates/copilot/src/copilot.rs | 6 +-- crates/feature_flags/src/feature_flags.rs | 16 +++----- crates/project_panel/src/file_associations.rs | 6 +-- crates/semantic_index/src/semantic_index.rs | 7 +--- crates/vim/src/mode_indicator.rs | 5 +-- crates/workspace/src/item.rs | 10 ++--- crates/workspace/src/workspace.rs | 11 ++---- crates/zed/src/main.rs | 16 ++++---- 10 files changed, 52 insertions(+), 70 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 06e445e3de96e4e03c560caee3fe4420dfbaed90..9f36665d871668920ed4ed8eb336f84fa6ba72d2 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -145,17 +145,16 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { let auto_updater = auto_updater.read(cx); let server_url = &auto_updater.server_url; let current_version = auto_updater.current_version; - if cx.has_global::() { - match cx.global::() { - ReleaseChannel::Dev => {} - ReleaseChannel::Nightly => {} - ReleaseChannel::Preview => { - cx.open_url(&format!("{server_url}/releases/preview/{current_version}")) - } - ReleaseChannel::Stable => { - cx.open_url(&format!("{server_url}/releases/stable/{current_version}")) - } - } + + if let Some(release_channel) = cx.try_global::() { + let channel = match release_channel { + ReleaseChannel::Preview => "preview", + ReleaseChannel::Stable => "stable", + _ => return, + }; + cx.open_url(&format!( + "{server_url}/releases/{channel}/{current_version}" + )) } } } @@ -257,11 +256,13 @@ impl AutoUpdater { "{server_url}/api/releases/latest?token={ZED_SECRET_CLIENT_TOKEN}&asset=Zed.dmg" ); cx.update(|cx| { - if cx.has_global::() { - if let Some(param) = cx.global::().release_query_param() { - url_string += "&"; - url_string += param; - } + if let Some(param) = cx + .try_global::() + .map(|release_channel| release_channel.release_query_param()) + .flatten() + { + url_string += "&"; + url_string += param; } })?; @@ -313,8 +314,8 @@ impl AutoUpdater { let (installation_id, release_channel, telemetry) = cx.update(|cx| { let installation_id = cx.global::>().telemetry().installation_id(); let release_channel = cx - .has_global::() - .then(|| cx.global::().display_name()); + .try_global::() + .map(|release_channel| release_channel.display_name()); let telemetry = TelemetrySettings::get_global(cx).metrics; (installation_id, release_channel, telemetry) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 5ee039a8cb23c939351c4ff85283569fe4d0dd95..313133ebef217f68e2817d8643c0c9ffcff1de39 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -150,11 +150,9 @@ const FLUSH_INTERVAL: Duration = Duration::from_secs(60 * 5); impl Telemetry { pub fn new(client: Arc, cx: &mut AppContext) -> Arc { - let release_channel = if cx.has_global::() { - Some(cx.global::().display_name()) - } else { - None - }; + let release_channel = cx + .try_global::() + .map(|release_channel| release_channel.display_name()); TelemetrySettings::register(cx); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 89d1086c8e4b897c3527964289ff11810078a57c..91204b74d7a48fab15952525eb219f76aa85c631 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -308,11 +308,7 @@ impl EventEmitter for Copilot {} impl Copilot { pub fn global(cx: &AppContext) -> Option> { - if cx.has_global::>() { - Some(cx.global::>().clone()) - } else { - None - } + cx.try_global::>().map(|model| model.clone()) } fn start( diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index ea16ff3f7291fd838be45fd826e7c7504ba914fd..907c37ddcd9649abb4feb0260e49f79e13af105f 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -57,18 +57,14 @@ impl FeatureFlagAppExt for AppContext { } fn has_flag(&self) -> bool { - if self.has_global::() { - self.global::().has_flag(T::NAME) - } else { - false - } + self.try_global::() + .map(|flags| flags.has_flag(T::NAME)) + .unwrap_or(false) } fn is_staff(&self) -> bool { - if self.has_global::() { - return self.global::().staff; - } else { - false - } + self.try_global::() + .map(|flags| flags.staff) + .unwrap_or(false) } } diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index 0ddcfc9285eb31b7f5be50d57d6ef8dd14cebb42..783fae4c5257a177845aa6e4cfdab30d058e7394 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -42,7 +42,7 @@ impl FileAssociations { } pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { - let this = cx.has_global::().then(|| cx.global::())?; + let this = cx.try_global::()?; // FIXME: Associate a type with the languages and have the file's language // override these associations @@ -58,7 +58,7 @@ impl FileAssociations { } pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option> { - let this = cx.has_global::().then(|| cx.global::())?; + let this = cx.try_global::()?; let key = if expanded { EXPANDED_DIRECTORY_TYPE @@ -72,7 +72,7 @@ impl FileAssociations { } pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option> { - let this = cx.has_global::().then(|| cx.global::())?; + let this = cx.try_global::()?; let key = if expanded { EXPANDED_CHEVRON_TYPE diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 801f02e600c1130eb5364e352f7f3823d6821f7a..a556986f9b1f9bda64706a8691cd329ead136086 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -275,11 +275,8 @@ pub struct SearchResult { impl SemanticIndex { pub fn global(cx: &mut AppContext) -> Option> { - if cx.has_global::>() { - Some(cx.global::>().clone()) - } else { - None - } + cx.try_global::>() + .map(|semantic_index| semantic_index.clone()) } pub fn authenticate(&mut self, cx: &mut AppContext) -> bool { diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index b669b16112874a89cd303499cc0d17eab6b99267..423ae0c4e17cef8dd8dfdead70c340dbd9cd1162 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -28,11 +28,10 @@ impl ModeIndicator { fn update_mode(&mut self, cx: &mut ViewContext) { // Vim doesn't exist in some tests - if !cx.has_global::() { + let Some(vim) = cx.try_global::() else { return; - } + }; - let vim = Vim::read(cx); if vim.enabled { self.mode = Some(vim.state().mode); } else { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 4f696e4a335040eebc10396262382f60c8f2821f..79742ee7322f4e760217fdee2af69256ca9edc82 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -586,13 +586,9 @@ impl ItemHandle for View { } fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { - if cx.has_global::() { - let builders = cx.global::(); - let item = self.to_any(); - Some(builders.get(&item.entity_type())?.1(&item)) - } else { - None - } + let builders = cx.try_global::()?; + let item = self.to_any(); + Some(builders.get(&item.entity_type())?.1(&item)) } fn on_release( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e8589849f14dbe9148ff467480f5d4a5583d1ed7..20c8bfc94a8adffb1eeb680d95c4f6dc602b34f5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -616,8 +616,8 @@ impl Workspace { let modal_layer = cx.new_view(|_| ModalLayer::new()); let mut active_call = None; - if cx.has_global::>() { - let call = cx.global::>().clone(); + if let Some(call) = cx.try_global::>() { + let call = call.clone(); let mut subscriptions = Vec::new(); subscriptions.push(cx.subscribe(&call, Self::on_active_call_event)); active_call = Some((call, subscriptions)); @@ -3686,11 +3686,8 @@ impl WorkspaceStore { update: proto::update_followers::Variant, cx: &AppContext, ) -> Option<()> { - if !cx.has_global::>() { - return None; - } - - let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id(); + let active_call = cx.try_global::>()?; + let room_id = active_call.read(cx).room()?.read(cx).id(); let follower_ids: Vec<_> = self .followers .iter() diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 821668001c4fa42f757eea55b5e80361e0456e6d..a7c52e592fcb93089a4f88351d19d377ef9ce0bc 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -102,13 +102,15 @@ fn main() { let open_listener = listener.clone(); app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); app.on_reopen(move |cx| { - if cx.has_global::>() { - if let Some(app_state) = cx.global::>().upgrade() { - workspace::open_new(&app_state, cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) - .detach(); - } + if let Some(app_state) = cx + .try_global::>() + .map(|app_state| app_state.upgrade()) + .flatten() + { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); } }); From aacb17ef38047e12548352d11b62402694891ce6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 18 Jan 2024 09:49:02 +0200 Subject: [PATCH 77/96] Fix buffer search focus not working When the Deploy action is called in the buffer with the buffer search bar already deployed, the focus should be on the search bar. --- crates/search/src/buffer_search.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index a1f0d9773be242818fa31cdb26a5a0de9e532dcd..f4c9f7ef0f43e5afc1237428d007a2ec0cf050ee 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -638,6 +638,12 @@ impl BufferSearchBar { registrar.register_handler(|this, _: &editor::actions::Cancel, cx| { this.dismiss(&Dismiss, cx); }); + + // register deploy buffer search for both search bar states, since we want to focus into the search bar + // when the deploy action is triggered in the buffer. + registrar.register_handler(|this, deploy, cx| { + this.deploy(deploy, cx); + }); registrar.register_handler_for_dismissed_search(|this, deploy, cx| { this.deploy(deploy, cx); }) From ed28170d428063bb64b23287c333368b9b721190 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 18 Jan 2024 10:04:38 +0100 Subject: [PATCH 78/96] Always synchronize terminal before rendering it Previously, we were trying not to synchronize the terminal too often because there could be multiple layout/paint calls prior to rendering a frame. Now that we perform a single render pass per frame, we can just synchronize the terminal state. Not doing so could make it seem like we're dropping frames. --- crates/terminal/src/terminal.rs | 35 ++------------------ crates/terminal_view/src/terminal_element.rs | 2 +- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3a01f01ca89e03a35586ed325db69a93844ed270..0b87ed1d976daf247b7b84efe3280290602911f3 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -47,7 +47,7 @@ use std::{ os::unix::prelude::AsRawFd, path::PathBuf, sync::Arc, - time::{Duration, Instant}, + time::Duration, }; use thiserror::Error; @@ -385,8 +385,6 @@ impl TerminalBuilder { last_content: Default::default(), last_mouse: None, matches: Vec::new(), - last_synced: Instant::now(), - sync_task: None, selection_head: None, shell_fd: fd as u32, shell_pid, @@ -542,8 +540,6 @@ pub struct Terminal { last_mouse_position: Option>, pub matches: Vec>, pub last_content: TerminalContent, - last_synced: Instant, - sync_task: Option>, pub selection_head: Option, pub breadcrumb_text: String, shell_pid: u32, @@ -977,40 +973,15 @@ impl Terminal { self.input(paste_text); } - pub fn try_sync(&mut self, cx: &mut ModelContext) { + pub fn sync(&mut self, cx: &mut ModelContext) { let term = self.term.clone(); - - let mut terminal = if let Some(term) = term.try_lock_unfair() { - term - } else if self.last_synced.elapsed().as_secs_f32() > 0.25 { - term.lock_unfair() // It's been too long, force block - } else if let None = self.sync_task { - //Skip this frame - let delay = cx.background_executor().timer(Duration::from_millis(16)); - self.sync_task = Some(cx.spawn(|weak_handle, mut cx| async move { - delay.await; - if let Some(handle) = weak_handle.upgrade() { - handle - .update(&mut cx, |terminal, cx| { - terminal.sync_task.take(); - cx.notify(); - }) - .ok(); - } - })); - return; - } else { - //No lock and delayed rendering already scheduled, nothing to do - return; - }; - + let mut terminal = term.lock_unfair(); //Note that the ordering of events matters for event processing while let Some(e) = self.events.pop_front() { self.process_terminal_event(&e, &mut terminal, cx) } self.last_content = Self::make_content(&terminal, &self.last_content); - self.last_synced = Instant::now(); } fn make_content(term: &Term, last_content: &TerminalContent) -> TerminalContent { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 6298b4c16a07b47054430a6e2dcacd9e4f6e033e..746a3716b82adbc7cbd0addba7c4d1d024eabc6c 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -446,7 +446,7 @@ impl TerminalElement { let last_hovered_word = self.terminal.update(cx, |terminal, cx| { terminal.set_size(dimensions); - terminal.try_sync(cx); + terminal.sync(cx); if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { terminal.last_content.last_hovered_word.clone() } else { From b6786d5e41e4dbe84cffe5fa8b6148370db8f539 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 18 Jan 2024 11:36:00 +0200 Subject: [PATCH 79/96] Use a proper action when clicking navigate forward button --- crates/workspace/src/pane.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 3e88469aa8593967e3ffea1cfd3ba0392f7e11f7..a9f1676c66c59da0482caebe8c9a4e3da1626804 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -195,7 +195,7 @@ struct NavHistoryState { next_timestamp: Arc, } -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub enum NavigationMode { Normal, GoingBack, @@ -1462,7 +1462,7 @@ impl Pane { .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) + move |_, cx| view.update(cx, Self::navigate_forward) }) .disabled(!self.can_navigate_forward()) .tooltip(|cx| Tooltip::for_action("Go Forward", &GoForward, cx)), From 559461923fba13d906f1e5135aa7d6c85dbe77fc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 18 Jan 2024 10:40:15 +0100 Subject: [PATCH 80/96] Remove unused `PlatformAtlas::clear` method --- crates/gpui/src/platform.rs | 2 -- crates/gpui/src/platform/mac/metal_atlas.rs | 14 -------------- crates/gpui/src/platform/test/window.rs | 6 ------ 3 files changed, 22 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index dfb85104fec3b542b221b527dbe2d82b336af868..f43e96280f43ef1ad1031eda9cfc13bdd6250cd6 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -282,8 +282,6 @@ pub(crate) trait PlatformAtlas: Send + Sync { key: &AtlasKey, build: &mut dyn FnMut() -> Result<(Size, Cow<'a, [u8]>)>, ) -> Result; - - fn clear(&self); } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index 10ca53530e2ba16d80f07cfab1722ed0eecbebc3..d3caeba5222e6a4739fc63ef54358eb3589debbf 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -74,20 +74,6 @@ impl PlatformAtlas for MetalAtlas { Ok(tile) } } - - fn clear(&self) { - let mut lock = self.0.lock(); - lock.tiles_by_key.clear(); - for texture in &mut lock.monochrome_textures { - texture.clear(); - } - for texture in &mut lock.polychrome_textures { - texture.clear(); - } - for texture in &mut lock.path_textures { - texture.clear(); - } - } } impl MetalAtlasState { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 5c8a3e5a59cf91b86d50c311c1beeccfd5ac2840..2f080bd7098bd42cf309c52e86df754e0d153a35 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -325,10 +325,4 @@ impl PlatformAtlas for TestAtlas { Ok(state.tiles[key].clone()) } - - fn clear(&self) { - let mut state = self.0.lock(); - state.tiles = HashMap::default(); - state.next_id = 0; - } } From 5e6d1a47b26098b2539e97b03955d76f118fc971 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 18 Jan 2024 10:59:32 +0100 Subject: [PATCH 81/96] Refactor LanguageSever::fake into FakeLanguageServer::new This is just moving code around and doesn't change behaviour, but it's something Julia and I bumped into yesterday while writing docs. --- crates/copilot/src/copilot.rs | 3 ++- crates/language/src/language.rs | 2 +- crates/lsp/src/lsp.rs | 39 ++++++++++++++++++--------------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 91204b74d7a48fab15952525eb219f76aa85c631..f36567c6b9439d59dd92ffb61aebaf93059e926b 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -369,10 +369,11 @@ impl Copilot { #[cfg(any(test, feature = "test-support"))] pub fn fake(cx: &mut gpui::TestAppContext) -> (Model, lsp::FakeLanguageServer) { + use lsp::FakeLanguageServer; use node_runtime::FakeNodeRuntime; let (server, fake_server) = - LanguageServer::fake("copilot".into(), Default::default(), cx.to_async()); + FakeLanguageServer::new("copilot".into(), Default::default(), cx.to_async()); let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); let node_runtime = FakeNodeRuntime::new(); let this = cx.new_model(|cx| Self { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 366d2b0098ca36437252044c50825bceaed96081..57b76eddadc69c10201d0f2ba2430a1ea4abb07d 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -951,7 +951,7 @@ impl LanguageRegistry { if language.fake_adapter.is_some() { let task = cx.spawn(|cx| async move { let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); - let (server, mut fake_server) = lsp::LanguageServer::fake( + let (server, mut fake_server) = lsp::FakeLanguageServer::new( fake_adapter.name.to_string(), fake_adapter.capabilities.clone(), cx.clone(), diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 80ab6b07d1d5869b343566b64a022115fe84f8ad..a70422008ccd86a4db70c33e4ac8cdface6c9217 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -972,30 +972,18 @@ pub struct FakeLanguageServer { } #[cfg(any(test, feature = "test-support"))] -impl LanguageServer { - pub fn full_capabilities() -> ServerCapabilities { - ServerCapabilities { - document_highlight_provider: Some(OneOf::Left(true)), - code_action_provider: Some(CodeActionProviderCapability::Simple(true)), - document_formatting_provider: Some(OneOf::Left(true)), - document_range_formatting_provider: Some(OneOf::Left(true)), - definition_provider: Some(OneOf::Left(true)), - type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), - ..Default::default() - } - } - +impl FakeLanguageServer { /// Construct a fake language server. - pub fn fake( + pub fn new( name: String, capabilities: ServerCapabilities, cx: AsyncAppContext, - ) -> (Self, FakeLanguageServer) { + ) -> (LanguageServer, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); let (notifications_tx, notifications_rx) = channel::unbounded(); - let server = Self::new_internal( + let server = LanguageServer::new_internal( LanguageServerId(0), stdin_writer, stdout_reader, @@ -1008,7 +996,7 @@ impl LanguageServer { |_| {}, ); let fake = FakeLanguageServer { - server: Arc::new(Self::new_internal( + server: Arc::new(LanguageServer::new_internal( LanguageServerId(0), stdout_writer, stdin_reader, @@ -1053,6 +1041,21 @@ impl LanguageServer { } } +#[cfg(any(test, feature = "test-support"))] +impl LanguageServer { + pub fn full_capabilities() -> ServerCapabilities { + ServerCapabilities { + document_highlight_provider: Some(OneOf::Left(true)), + code_action_provider: Some(CodeActionProviderCapability::Simple(true)), + document_formatting_provider: Some(OneOf::Left(true)), + document_range_formatting_provider: Some(OneOf::Left(true)), + definition_provider: Some(OneOf::Left(true)), + type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), + ..Default::default() + } + } +} + #[cfg(any(test, feature = "test-support"))] impl FakeLanguageServer { /// See [`LanguageServer::notify`]. @@ -1188,7 +1191,7 @@ mod tests { #[gpui::test] async fn test_fake(cx: &mut TestAppContext) { let (server, mut fake) = - LanguageServer::fake("the-lsp".to_string(), Default::default(), cx.to_async()); + FakeLanguageServer::new("the-lsp".to_string(), Default::default(), cx.to_async()); let (message_tx, message_rx) = channel::unbounded(); let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); From 0a0921f88b4540f2e5cef0686667942bf20a5fa7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:01:10 +0100 Subject: [PATCH 82/96] gpui: Bring back family and style names in font name suggestions --- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/mac/text_system.rs | 12 +++++++++++- crates/gpui/src/text_system.rs | 19 +++++++++++-------- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f43e96280f43ef1ad1031eda9cfc13bdd6250cd6..e08d7a85521c9f24d4c25ab6decb1f3fb99d9882 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -201,6 +201,7 @@ pub trait PlatformDispatcher: Send + Sync { pub(crate) trait PlatformTextSystem: Send + Sync { fn add_fonts(&self, fonts: &[Arc>]) -> Result<()>; fn all_font_names(&self) -> Vec; + fn all_font_families(&self) -> Vec; fn font_id(&self, descriptor: &Font) -> Result; fn font_metrics(&self, font_id: FontId) -> FontMetrics; fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result>; diff --git a/crates/gpui/src/platform/mac/text_system.rs b/crates/gpui/src/platform/mac/text_system.rs index a77741074f13e1515198921b95a3a310e4d3d422..d11efa902ae7c270ff0331b74b1860032ff5f212 100644 --- a/crates/gpui/src/platform/mac/text_system.rs +++ b/crates/gpui/src/platform/mac/text_system.rs @@ -85,7 +85,9 @@ impl PlatformTextSystem for MacTextSystem { }; let mut names = BTreeSet::new(); for descriptor in descriptors.into_iter() { - names.insert(descriptor.display_name()); + names.insert(descriptor.font_name()); + names.insert(descriptor.family_name()); + names.insert(descriptor.style_name()); } if let Ok(fonts_in_memory) = self.0.read().memory_source.all_families() { names.extend(fonts_in_memory); @@ -93,6 +95,14 @@ impl PlatformTextSystem for MacTextSystem { names.into_iter().collect() } + fn all_font_families(&self) -> Vec { + self.0 + .read() + .system_source + .all_families() + .expect("core text should never return an error") + } + fn font_id(&self, font: &Font) -> Result { let lock = self.0.upgradable_read(); if let Some(font_id) = lock.font_selections.get(font) { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 27d216dd50e673659e9873908d7a8f54364a256f..1c9de5ea0495f60fc8d219891ad8a0c7406ae9fe 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -13,7 +13,7 @@ use crate::{ SharedString, Size, UnderlineStyle, }; use anyhow::anyhow; -use collections::{FxHashMap, FxHashSet}; +use collections::{BTreeSet, FxHashMap, FxHashSet}; use core::fmt; use itertools::Itertools; use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard}; @@ -66,15 +66,18 @@ impl TextSystem { } pub fn all_font_names(&self) -> Vec { - let mut families = self.platform_text_system.all_font_names(); - families.append( - &mut self - .fallback_font_stack + let mut names: BTreeSet<_> = self + .platform_text_system + .all_font_names() + .into_iter() + .collect(); + names.extend(self.platform_text_system.all_font_families().into_iter()); + names.extend( + self.fallback_font_stack .iter() - .map(|font| font.family.to_string()) - .collect(), + .map(|font| font.family.to_string()), ); - families + names.into_iter().collect() } pub fn add_fonts(&self, fonts: &[Arc>]) -> Result<()> { self.platform_text_system.add_fonts(fonts) From 7c5fdb3a44364c4b052ec2a42afafff1f2b693e5 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 18 Jan 2024 08:07:52 -0500 Subject: [PATCH 83/96] Clean up view_release_notes() --- crates/auto_update/src/auto_update.rs | 30 ++++++++++--------- crates/auto_update/src/update_notification.rs | 4 ++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 9f36665d871668920ed4ed8eb336f84fa6ba72d2..3b8d1c6e61e2f42839930ac49311c8f60a38e341 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -93,7 +93,9 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo cx.observe_new_views(|workspace: &mut Workspace, _cx| { workspace.register_action(|_, action: &Check, cx| check(action, cx)); - workspace.register_action(|_, action, cx| view_release_notes(action, cx)); + workspace.register_action(|_, action, cx| { + view_release_notes(action, cx); + }); // @nate - code to trigger update notification on launch // todo!("remove this when Nate is done") @@ -140,23 +142,23 @@ pub fn check(_: &Check, cx: &mut WindowContext) { } } -pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { - if let Some(auto_updater) = AutoUpdater::get(cx) { +pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<()> { + let auto_updater = AutoUpdater::get(cx)?; + let release_channel = cx.try_global::()?; + + if matches!( + release_channel, + ReleaseChannel::Stable | ReleaseChannel::Preview + ) { let auto_updater = auto_updater.read(cx); let server_url = &auto_updater.server_url; + let release_channel = release_channel.dev_name(); let current_version = auto_updater.current_version; - - if let Some(release_channel) = cx.try_global::() { - let channel = match release_channel { - ReleaseChannel::Preview => "preview", - ReleaseChannel::Stable => "stable", - _ => return, - }; - cx.open_url(&format!( - "{server_url}/releases/{channel}/{current_version}" - )) - } + let url = format!("{server_url}/releases/{release_channel}/{current_version}"); + cx.open_url(&url); } + + None } pub fn notify_of_any_new_update(cx: &mut ViewContext) -> Option<()> { diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index 985972da56364c646850fe7146f3323d06f56015..1562d90057199b14ccafe60bfa13ac0506a6dd27 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -40,7 +40,9 @@ impl Render for UpdateNotification { .id("notes") .child(Label::new("View the release notes")) .cursor_pointer() - .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), + .on_click(|_, cx| { + crate::view_release_notes(&Default::default(), cx); + }), ) } } From b6d8665fc1bcf005278c6aea5b4dde9ab06debb2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 18 Jan 2024 14:07:53 +0100 Subject: [PATCH 84/96] pane: stop propagation of drag/click events in resizing handle This prevents focused editor from being scrolled while a pane is getting resized. Fixes: Mouse down to start an editor resize causes a scroll --- crates/workspace/src/pane_group.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index e631cd9c436b6918a974d2afdcc7ac9d4aa37520..476592f3745568e67fcbea9b44060cceea90370c 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -698,6 +698,7 @@ mod element { workspace .update(cx, |this, cx| this.schedule_serialize(cx)) .log_err(); + cx.stop_propagation(); cx.refresh(); } @@ -754,8 +755,10 @@ mod element { workspace .update(cx, |this, cx| this.schedule_serialize(cx)) .log_err(); + cx.refresh(); } + cx.stop_propagation(); } } }); From f9165938b0d25f2238f56895732f7aa47a56faed Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 18 Jan 2024 07:01:46 -0700 Subject: [PATCH 85/96] More adjustments for blog post --- crates/gpui/examples/ownership_post.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/gpui/examples/ownership_post.rs b/crates/gpui/examples/ownership_post.rs index 603b63b254644f808bebb0477d569a58f9dfea70..cd3b6264c36537ff61d84556201b801b887ab26b 100644 --- a/crates/gpui/examples/ownership_post.rs +++ b/crates/gpui/examples/ownership_post.rs @@ -4,12 +4,18 @@ struct Counter { count: usize, } +struct Change { + increment: usize, +} + +impl EventEmitter for Counter {} + fn main() { App::new().run(|cx: &mut AppContext| { let counter: Model = cx.new_model(|_cx| Counter { count: 0 }); - let observer = cx.new_model(|cx: &mut ModelContext| { - cx.observe(&counter, |observer, observed, cx| { - observer.count = observed.read(cx).count * 2; + let subscriber = cx.new_model(|cx: &mut ModelContext| { + cx.subscribe(&counter, |subscriber, _emitter, event, _cx| { + subscriber.count += event.increment * 2; }) .detach(); @@ -19,16 +25,11 @@ fn main() { }); counter.update(cx, |counter, cx| { - counter.count += 1; + counter.count += 2; cx.notify(); + cx.emit(Change { increment: 2 }); }); - assert_eq!(observer.read(cx).count, 2); + assert_eq!(subscriber.read(cx).count, 4); }); } - -struct Change { - delta: isize, -} - -impl EventEmitter for Counter {} From 130ea79c95611be311db2299d1acf78e01e5987e Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 18 Jan 2024 15:04:37 +0100 Subject: [PATCH 86/96] Fix focus and re-focus of project-wide search This fixes the issue of `Cmd-shift-f` not refocusing the search bar when a project-wide search already exists. It also fixes the handlers for "search in new" and "new search". Co-authored-by: Kirill --- crates/search/src/project_search.rs | 50 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index efb7af3879dcdff0141897b65fb0e3b952753609..c1ebcfe0a6a4a8214d6cbc76b3ebca5e0e1f5125 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -94,31 +94,29 @@ pub fn init(cx: &mut AppContext) { search_bar.select_next_match(action, cx) }, ); - register_workspace_action( - workspace, - move |search_bar, action: &SelectPrevMatch, cx| { - search_bar.select_prev_match(action, cx) - }, - ); - register_workspace_action_for_dismissed_search( - workspace, - move |workspace, action: &NewSearch, cx| { - ProjectSearchView::new_search(workspace, action, cx) - }, - ); - register_workspace_action_for_dismissed_search( - workspace, - move |workspace, action: &DeploySearch, cx| { - ProjectSearchView::deploy_search(workspace, action, cx) - }, - ); - register_workspace_action_for_dismissed_search( - workspace, - move |workspace, action: &SearchInNew, cx| { - ProjectSearchView::search_in_new(workspace, action, cx) - }, - ); + // Only handle search_in_new if there is a search present + register_workspace_action_for_present_search(workspace, |workspace, action, cx| { + ProjectSearchView::search_in_new(workspace, action, cx) + }); + + // Both on present and dismissed search, we need to unconditionally handle those actions to focus from the editor. + workspace.register_action(move |workspace, action: &DeploySearch, cx| { + if workspace.has_active_modal(cx) { + cx.propagate(); + return; + } + ProjectSearchView::deploy_search(workspace, action, cx); + cx.notify(); + }); + workspace.register_action(move |workspace, action: &NewSearch, cx| { + if workspace.has_active_modal(cx) { + cx.propagate(); + return; + } + ProjectSearchView::new_search(workspace, action, cx); + cx.notify(); + }); }) .detach(); } @@ -2057,7 +2055,7 @@ fn register_workspace_action( }); } -fn register_workspace_action_for_dismissed_search( +fn register_workspace_action_for_present_search( workspace: &mut Workspace, callback: fn(&mut Workspace, &A, &mut ViewContext), ) { @@ -2073,7 +2071,7 @@ fn register_workspace_action_for_dismissed_search( .toolbar() .read(cx) .item_of_type::() - .map(|search_bar| search_bar.read(cx).active_project_search.is_none()) + .map(|search_bar| search_bar.read(cx).active_project_search.is_some()) .unwrap_or(false); if should_notify { callback(workspace, action, cx); From 36ed5a31dfd1e591480be8c7550b97b479aa337b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 18 Jan 2024 16:54:35 +0200 Subject: [PATCH 87/96] Wrap over picker's matches when reaching the end of the list --- crates/picker/src/picker.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 0af4675c9803505a4848049233fb02b60b308632..78d1c09e03a5e76fb86b57db248fcf1629a4d4ff 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -4,7 +4,7 @@ use gpui::{ FocusableView, Length, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; -use std::{cmp, sync::Arc}; +use std::sync::Arc; use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing, ListSeparator}; use workspace::ModalView; @@ -103,7 +103,7 @@ impl Picker { let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); - let ix = cmp::min(index + 1, count - 1); + let ix = if index == count - 1 { 0 } else { index + 1 }; self.delegate.set_selected_index(ix, cx); self.scroll_handle.scroll_to_item(ix); cx.notify(); @@ -114,7 +114,7 @@ impl Picker { let count = self.delegate.match_count(); if count > 0 { let index = self.delegate.selected_index(); - let ix = index.saturating_sub(1); + let ix = if index == 0 { count - 1 } else { index - 1 }; self.delegate.set_selected_index(ix, cx); self.scroll_handle.scroll_to_item(ix); cx.notify(); From b30efc9e81c75d5f02bd88930ec2d3cda8d79d11 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 18 Jan 2024 16:26:20 +0100 Subject: [PATCH 88/96] Fix missing icons: set svg_renderer when assets are updated Co-authored-by: Mikayla --- crates/gpui/src/app.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 477bea143916688b84571b2da3ad24b9bbec52cf..c7a6a92a17dce204412fa2c088132789d33b0cc4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -121,7 +121,11 @@ impl App { /// Assign pub fn with_assets(self, asset_source: impl AssetSource) -> Self { - self.0.borrow_mut().asset_source = Arc::new(asset_source); + let mut context_lock = self.0.borrow_mut(); + let asset_source = Arc::new(asset_source); + context_lock.asset_source = asset_source.clone(); + context_lock.svg_renderer = SvgRenderer::new(asset_source); + drop(context_lock); self } From 9506fa461f49de679302809664916af99b97ce33 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:32:31 +0100 Subject: [PATCH 89/96] Start documenting display_map module --- crates/editor/src/display_map.rs | 37 ++++++++++++----------- crates/editor/src/display_map/fold_map.rs | 10 +++--- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 7ab5b0ff2abe75b6a35ab2ed120c2dee55e489af..7f29a7d04f536ac8de4af01cf2368e7be948b054 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1,3 +1,22 @@ +//! This module defines where the text should be displayed in an [`Editor`][Editor]. +//! +//! Not literally though - rendering, layout and all that jazz is a responsibility of [`EditorElement`][EditorElement]. +//! Instead, [`DisplayMap`] decides where Inlays/Inlay hints are displayed, when +//! to apply a soft wrap, where to add fold indicators, whether there are any tabs in the buffer that +//! we display as spaces and where to display custom blocks (like diagnostics). +//! Seems like a lot? That's because it is. [`DisplayMap`] is conceptually made up +//! of several smaller structures that form a hierarchy (starting at the bottom): +//! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed. +//! - [`FoldMap`] that decides where the fold indicators should be; it also tracks parts of a source file that are currently folded. +//! - [`TabMap`] that keeps track of hard tabs in a buffer. +//! - [`WrapMap`] that handles soft wrapping. +//! - [`BlockMap`] that tracks custom blocks such as diagnostics that should be displayed within buffer. +//! - [`DisplayMap`] that adds background highlights to the regions of text. +//! Each one of those builds on top of preceding map. +//! +//! [Editor]: crate::Editor +//! [EditorElement]: crate::element::EditorElement + mod block_map; mod fold_map; mod inlay_map; @@ -971,24 +990,6 @@ impl ToDisplayPoint for Anchor { } } -pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator { - let max_row = display_map.max_point().row(); - let start_row = display_row + 1; - let mut current = None; - std::iter::from_fn(move || { - if current == None { - current = Some(start_row); - } else { - current = Some(current.unwrap() + 1) - } - if current.unwrap() > max_row { - None - } else { - current - } - }) -} - #[cfg(test)] pub mod tests { use super::*; diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 7c6eeb444eb69ca7557a6d55ab4b4b1125b8b078..61b973cc6c70cd8ff733753c23f22474f58fce82 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -71,10 +71,10 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint { } } -pub struct FoldMapWriter<'a>(&'a mut FoldMap); +pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap); impl<'a> FoldMapWriter<'a> { - pub fn fold( + pub(crate) fn fold( &mut self, ranges: impl IntoIterator>, ) -> (FoldSnapshot, Vec) { @@ -129,7 +129,7 @@ impl<'a> FoldMapWriter<'a> { (self.0.snapshot.clone(), edits) } - pub fn unfold( + pub(crate) fn unfold( &mut self, ranges: impl IntoIterator>, inclusive: bool, @@ -178,14 +178,14 @@ impl<'a> FoldMapWriter<'a> { } } -pub struct FoldMap { +pub(crate) struct FoldMap { snapshot: FoldSnapshot, ellipses_color: Option, next_fold_id: FoldId, } impl FoldMap { - pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) { + pub(crate) fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) { let this = Self { snapshot: FoldSnapshot { folds: Default::default(), From 7b9e7fea4e8ad5195500b9014eb6987939ac626e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 18 Jan 2024 11:12:45 -0500 Subject: [PATCH 90/96] Use regular info color for speaker borders (#4126) This PR updates the speaker borders to use the regular `info` status color instead of the `info_border` color. This should provide more contrast and make it clearer as to who is speaking. Release Notes: - Made the active speakers' borders more visible when in a call. --- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 01f0ccb179fd9f870bcad138aee2fd7a9d925987..ca988a9b1ad27556a988b020dad376e970838af1 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -486,7 +486,7 @@ impl CollabTitlebarItem { Avatar::new(user.avatar_uri.clone()) .grayscale(!is_present) .border_color(if is_speaking { - cx.theme().status().info_border + cx.theme().status().info } else { // We draw the border in a transparent color rather to avoid // the layout shift that would come with adding/removing the border. From 2835c9a972a082a4818ced141465f184d355049e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Jan 2024 10:40:43 -0700 Subject: [PATCH 91/96] Don't send follower events from other panes Co-Authored-By: Mikayla --- crates/workspace/src/item.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 79742ee7322f4e760217fdee2af69256ca9edc82..908ea1d168c22fcfbeeb0417bd40baec9f0ae1c9 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -448,11 +448,13 @@ impl ItemHandle for View { workspace.unfollow(&pane, cx); } - if item.add_event_to_update_proto( - event, - &mut *pending_update.borrow_mut(), - cx, - ) && !pending_update_scheduled.load(Ordering::SeqCst) + if item.focus_handle(cx).contains_focused(cx) + && item.add_event_to_update_proto( + event, + &mut *pending_update.borrow_mut(), + cx, + ) + && !pending_update_scheduled.load(Ordering::SeqCst) { pending_update_scheduled.store(true, Ordering::SeqCst); cx.defer({ From 2e35d900e0132a3a95529d6815ad867da00ae3ef Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 18 Jan 2024 10:13:03 -0800 Subject: [PATCH 92/96] Invoke pane's focus_in handler when pane is focused directly --- crates/workspace/src/pane.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a1f3e6992aef51291fc66b9f4092e3dbd24c7be6..e8dbc746944e54b962fa99cee6872b36c4597360 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -239,6 +239,7 @@ impl Pane { let focus_handle = cx.focus_handle(); let subscriptions = vec![ + cx.on_focus(&focus_handle, Pane::focus_in), cx.on_focus_in(&focus_handle, Pane::focus_in), cx.on_focus_out(&focus_handle, Pane::focus_out), ]; From bc2302f72388c2d14ccd301c9dc613144b7c7a2a Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 18 Jan 2024 10:59:23 -0500 Subject: [PATCH 93/96] Store a z-index id per-layer Co-Authored-By: Nathan Sobo --- crates/collab_ui/src/face_pile.rs | 2 +- crates/gpui/src/style.rs | 8 +-- crates/gpui/src/styled.rs | 2 +- crates/gpui/src/window.rs | 71 +++++++++++++------------ crates/storybook/src/stories/z_index.rs | 4 +- crates/ui/src/styles/elevation.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 7 files changed, 46 insertions(+), 45 deletions(-) diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index fb6c59cc8079073acbb6b481e214b54619f9eea6..31132b298148944a95cde28918c51bcf94c33766 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -13,7 +13,7 @@ impl RenderOnce for FacePile { let isnt_last = ix < player_count - 1; div() - .z_index((player_count - ix) as u8) + .z_index((player_count - ix) as u16) .when(isnt_last, |div| div.neg_mr_1()) .child(player) }); diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 095233280edefc0b11d85e3a4ee255f54c8da13d..bfc36ef6b116f355be5911151a32831cbd73f362 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -110,7 +110,7 @@ pub struct Style { /// The mouse cursor style shown when the mouse pointer is over an element. pub mouse_cursor: Option, - pub z_index: Option, + pub z_index: Option, #[cfg(debug_assertions)] pub debug: bool, @@ -386,7 +386,7 @@ impl Style { let background_color = self.background.as_ref().and_then(Fill::color); if background_color.map_or(false, |color| !color.is_transparent()) { - cx.with_z_index(0, |cx| { + cx.with_z_index(1, |cx| { let mut border_color = background_color.unwrap_or_default(); border_color.a = 0.; cx.paint_quad(quad( @@ -399,12 +399,12 @@ impl Style { }); } - cx.with_z_index(0, |cx| { + cx.with_z_index(2, |cx| { continuation(cx); }); if self.is_border_visible() { - cx.with_z_index(0, |cx| { + cx.with_z_index(3, |cx| { let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size); let border_widths = self.border_widths.to_pixels(rem_size); let max_border_width = border_widths.max(); diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index 0eba1771f52d47bde32f465a887e52547f3a89b2..e8800d1ce9dea7831e7c5ab186c96def45434d1b 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -12,7 +12,7 @@ pub trait Styled: Sized { gpui_macros::style_helpers!(); - fn z_index(mut self, z_index: u8) -> Self { + fn z_index(mut self, z_index: u16) -> Self { self.style().z_index = Some(z_index); self } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 7453391ee721c1829e2140c4590f4bf67ce86187..c61379e814656b45e96de5fa506d90fe6cb6d43c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -43,30 +43,23 @@ use std::{ }; use util::{post_inc, ResultExt}; -const ACTIVE_DRAG_Z_INDEX: u8 = 1; +const ACTIVE_DRAG_Z_INDEX: u16 = 1; /// A global stacking order, which is created by stacking successive z-index values. /// Each z-index will always be interpreted in the context of its parent z-index. -#[derive(Deref, DerefMut, Clone, Ord, PartialOrd, PartialEq, Eq, Default)] -pub struct StackingOrder { - #[deref] - #[deref_mut] - context_stack: SmallVec<[u8; 64]>, - id: u32, +#[derive(Debug, Deref, DerefMut, Clone, Ord, PartialOrd, PartialEq, Eq, Default)] +pub struct StackingOrder(SmallVec<[StackingContext; 64]>); + +/// A single entry in a primitive's z-index stacking order +#[derive(Clone, Ord, PartialOrd, PartialEq, Eq, Default)] +pub struct StackingContext { + z_index: u16, + id: u16, } -impl std::fmt::Debug for StackingOrder { +impl std::fmt::Debug for StackingContext { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut stacks = self.context_stack.iter().peekable(); - write!(f, "[({}): ", self.id)?; - while let Some(z_index) = stacks.next() { - write!(f, "{z_index}")?; - if stacks.peek().is_some() { - write!(f, "->")?; - } - } - write!(f, "]")?; - Ok(()) + write!(f, "{{{}.{}}} ", self.z_index, self.id) } } @@ -315,8 +308,8 @@ pub(crate) struct Frame { pub(crate) scene: Scene, pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds)>, pub(crate) z_index_stack: StackingOrder, - pub(crate) next_stacking_order_id: u32, - next_root_z_index: u8, + next_stacking_order_id: u16, + next_root_z_index: u16, content_mask_stack: Vec>, element_offset_stack: Vec>, requested_input_handler: Option, @@ -1105,7 +1098,11 @@ impl<'a> WindowContext<'a> { if level >= opaque_level { break; } - if opaque_level.starts_with(&[ACTIVE_DRAG_Z_INDEX]) { + if opaque_level + .first() + .map(|c| c.z_index == ACTIVE_DRAG_Z_INDEX) + .unwrap_or(false) + { continue; } @@ -2452,36 +2449,40 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { size: self.window().viewport_size, }, }; + + let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index); let new_stacking_order_id = post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); - let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index); + let new_context = StackingContext { + z_index: new_root_z_index, + id: new_stacking_order_id, + }; + let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack); - self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; - self.window_mut() - .next_frame - .z_index_stack - .push(new_root_z_index); + + self.window_mut().next_frame.z_index_stack.push(new_context); self.window_mut().next_frame.content_mask_stack.push(mask); let result = f(self); self.window_mut().next_frame.content_mask_stack.pop(); self.window_mut().next_frame.z_index_stack = old_stacking_order; + result } /// Called during painting to invoke the given closure in a new stacking context. The given /// z-index is interpreted relative to the previous call to `stack`. - fn with_z_index(&mut self, z_index: u8, f: impl FnOnce(&mut Self) -> R) -> R { + fn with_z_index(&mut self, z_index: u16, f: impl FnOnce(&mut Self) -> R) -> R { let new_stacking_order_id = post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); - let old_stacking_order_id = mem::replace( - &mut self.window_mut().next_frame.z_index_stack.id, - new_stacking_order_id, - ); - self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; - self.window_mut().next_frame.z_index_stack.push(z_index); + let new_context = StackingContext { + z_index, + id: new_stacking_order_id, + }; + + self.window_mut().next_frame.z_index_stack.push(new_context); let result = f(self); - self.window_mut().next_frame.z_index_stack.id = old_stacking_order_id; self.window_mut().next_frame.z_index_stack.pop(); + result } diff --git a/crates/storybook/src/stories/z_index.rs b/crates/storybook/src/stories/z_index.rs index b6e49bfae32b046242e96a5de34d30c3be806b94..63ee1af7591ee8a62f07f052b320db8a70077b9d 100644 --- a/crates/storybook/src/stories/z_index.rs +++ b/crates/storybook/src/stories/z_index.rs @@ -76,7 +76,7 @@ impl Styles for Div {} #[derive(IntoElement)] struct ZIndexExample { - z_index: u8, + z_index: u16, } impl RenderOnce for ZIndexExample { @@ -166,7 +166,7 @@ impl RenderOnce for ZIndexExample { } impl ZIndexExample { - pub fn new(z_index: u8) -> Self { + pub fn new(z_index: u16) -> Self { Self { z_index } } } diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index 0aa3786a279242c8a3a301b497a60ab0c2c537ec..c2605fd152df49d04db57d6784bc3a54aaefd80d 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -20,7 +20,7 @@ pub enum ElevationIndex { } impl ElevationIndex { - pub fn z_index(self) -> u8 { + pub fn z_index(self) -> u16 { match self { ElevationIndex::Background => 0, ElevationIndex::Surface => 42, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 20c8bfc94a8adffb1eeb680d95c4f6dc602b34f5..a8aaa403fbf5d4cc11af3abe10f567f049138035 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4334,7 +4334,7 @@ impl Element for DisconnectedOverlay { } fn paint(&mut self, bounds: Bounds, overlay: &mut Self::State, cx: &mut WindowContext) { - cx.with_z_index(u8::MAX, |cx| { + cx.with_z_index(u16::MAX, |cx| { cx.add_opaque_layer(bounds); overlay.paint(cx); }) From e992f84735a2ca166a7388d4eca69a6b4e6ee212 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 18 Jan 2024 10:29:05 -0800 Subject: [PATCH 94/96] collab 0.37.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4c3a4e8592e86cc3ce2c795baa9b1b62fd8b2045..62dbd027dd47c2d99e6f2295d63d6a8ce3b6ea8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1452,7 +1452,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.36.1" +version = "0.37.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index bc273cb12a9b893ac0e0b7d0b710416ea4ba37b8..9209d9ac2d3e24ffec20b628076e1c3709d92bb6 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.36.1" +version = "0.37.0" publish = false [[bin]] From 920eced1d5e4fa40284b24c79db43153e87b7c64 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Jan 2024 12:07:52 -0700 Subject: [PATCH 95/96] Fix right click handler for tabs Also, some fun test helpers Co-Authored-By: Mikayla --- crates/collab/src/tests/integration_tests.rs | 44 ++++++++++++++++++- crates/gpui/src/app/test_context.rs | 12 ++++- crates/gpui/src/elements/div.rs | 23 ++++++++++ crates/gpui/src/window.rs | 22 +++++++++- .../ui/src/components/button/button_like.rs | 2 +- .../ui/src/components/button/icon_button.rs | 6 ++- crates/ui/src/components/context_menu.rs | 1 + crates/ui/src/components/right_click_menu.rs | 12 +++-- crates/ui/src/components/tab.rs | 5 ++- 9 files changed, 116 insertions(+), 11 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e68fd10d8d16b29300c3fa658596468df06be880..a8e52f4094c83ac79a29c9b30eae666566de96e2 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7,7 +7,10 @@ use client::{User, RECEIVE_TIMEOUT}; use collections::{HashMap, HashSet}; use fs::{repository::GitFileStatus, FakeFs, Fs as _, RemoveOptions}; use futures::StreamExt as _; -use gpui::{AppContext, BackgroundExecutor, Model, TestAppContext}; +use gpui::{ + px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent, + TestAppContext, +}; use language::{ language_settings::{AllLanguageSettings, Formatter}, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, @@ -5903,3 +5906,42 @@ async fn test_join_call_after_screen_was_shared( ); }); } + +#[gpui::test] +async fn test_right_click_menu_behind_collab_panel(cx: &mut TestAppContext) { + let mut server = TestServer::start(cx.executor().clone()).await; + let client_a = server.create_client(cx, "user_a").await; + let (_workspace_a, cx) = client_a.build_test_workspace(cx).await; + + cx.simulate_resize(size(px(300.), px(300.))); + + cx.simulate_keystrokes("cmd-n cmd-n cmd-n"); + cx.update(|cx| cx.refresh()); + + let tab_bounds = cx.debug_bounds("TAB-2").unwrap(); + let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap(); + + assert!( + tab_bounds.intersects(&new_tab_button_bounds), + "Tab should overlap with the new tab button, if this is failing check if there's been a redesign!" + ); + + cx.simulate_event(MouseDownEvent { + button: MouseButton::Right, + position: new_tab_button_bounds.center(), + modifiers: Modifiers::default(), + click_count: 1, + }); + + // regression test that the right click menu for tabs does not open. + assert!(cx.debug_bounds("MENU_ITEM-Close").is_none()); + + let tab_bounds = cx.debug_bounds("TAB-1").unwrap(); + cx.simulate_event(MouseDownEvent { + button: MouseButton::Right, + position: tab_bounds.center(), + modifiers: Modifiers::default(), + click_count: 1, + }); + assert!(cx.debug_bounds("MENU_ITEM-Close").is_some()); +} diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index d95558f058a91bb4a7cd9a3ab347ee10584bffba..d11c1239dd576d9db944f605cad7d4140624feb9 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -2,7 +2,7 @@ use crate::{ Action, AnyElement, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - AvailableSpace, BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, + AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor, InputEvent, Keystroke, Model, ModelContext, Pixels, Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, TextSystem, View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, @@ -618,6 +618,16 @@ impl<'a> VisualTestContext { self.cx.simulate_input(self.window, input) } + /// Simulates the user resizing the window to the new size. + pub fn simulate_resize(&self, size: Size) { + self.simulate_window_resize(self.window, size) + } + + /// debug_bounds returns the bounds of the element with the given selector. + pub fn debug_bounds(&mut self, selector: &'static str) -> Option> { + self.update(|cx| cx.window.rendered_frame.debug_bounds.get(selector).copied()) + } + /// Draw an element to the window. Useful for simulating events or actions pub fn draw( &mut self, diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 74000da0512ffac35e4cf87c122bdecf08d67aed..aa912eadbe9c986969dd197927af8963a3c58d0c 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -416,6 +416,18 @@ pub trait InteractiveElement: Sized { self } + #[cfg(any(test, feature = "test-support"))] + fn debug_selector(mut self, f: impl FnOnce() -> String) -> Self { + self.interactivity().debug_selector = Some(f()); + self + } + + #[cfg(not(any(test, feature = "test-support")))] + #[inline] + fn debug_selector(self, _: impl FnOnce() -> String) -> Self { + self + } + fn capture_any_mouse_down( mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, @@ -911,6 +923,9 @@ pub struct Interactivity { #[cfg(debug_assertions)] pub location: Option>, + + #[cfg(any(test, feature = "test-support"))] + pub debug_selector: Option, } #[derive(Clone, Debug)] @@ -980,6 +995,14 @@ impl Interactivity { let style = self.compute_style(Some(bounds), element_state, cx); let z_index = style.z_index.unwrap_or(0); + #[cfg(any(feature = "test-support", test))] + if let Some(debug_selector) = &self.debug_selector { + cx.window + .next_frame + .debug_bounds + .insert(debug_selector.clone(), bounds); + } + let paint_hover_group_handler = |cx: &mut WindowContext| { let hover_group_bounds = self .group_hover_style diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 7453391ee721c1829e2140c4590f4bf67ce86187..2329a5251ed010791e043bc08b70fb2e5e19d895 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -30,7 +30,7 @@ use std::{ borrow::{Borrow, BorrowMut, Cow}, cell::RefCell, collections::hash_map::Entry, - fmt::Debug, + fmt::{Debug, Display}, future::Future, hash::{Hash, Hasher}, marker::PhantomData, @@ -325,6 +325,9 @@ pub(crate) struct Frame { requested_cursor_style: Option, pub(crate) view_stack: Vec, pub(crate) reused_views: FxHashSet, + + #[cfg(any(test, feature = "test-support"))] + pub(crate) debug_bounds: collections::FxHashMap>, } impl Frame { @@ -348,6 +351,9 @@ impl Frame { requested_cursor_style: None, view_stack: Vec::new(), reused_views: FxHashSet::default(), + + #[cfg(any(test, feature = "test-support"))] + debug_bounds: FxHashMap::default(), } } @@ -3379,6 +3385,20 @@ pub enum ElementId { NamedInteger(SharedString, usize), } +impl Display for ElementId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ElementId::View(entity_id) => write!(f, "view-{}", entity_id)?, + ElementId::Integer(ix) => write!(f, "{}", ix)?, + ElementId::Name(name) => write!(f, "{}", name)?, + ElementId::FocusHandle(__) => write!(f, "FocusHandle")?, + ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?, + } + + Ok(()) + } +} + impl ElementId { pub(crate) fn from_entity_id(entity_id: EntityId) -> Self { ElementId::View(entity_id) diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index c2910acfc04bee67630c3d55b2ce2394559379f6..aafb33cd6f541a7573f7910fc559a8d170416689 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -293,7 +293,7 @@ impl ButtonSize { /// This is also used to build the prebuilt buttons. #[derive(IntoElement)] pub struct ButtonLike { - base: Div, + pub base: Div, id: ElementId, pub(super) style: ButtonStyle, pub(super) disabled: bool, diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index cc1e31b65cb0836be9a307107302ba8705597d6c..6de32c0eab29a22da541e2617d1adbe60f3656b2 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -24,14 +24,16 @@ pub struct IconButton { impl IconButton { pub fn new(id: impl Into, icon: IconName) -> Self { - Self { + let mut this = Self { base: ButtonLike::new(id), shape: IconButtonShape::Wide, icon, icon_size: IconSize::default(), icon_color: Color::Default, selected_icon: None, - } + }; + this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon)); + this } pub fn shape(mut self, shape: IconButtonShape) -> Self { diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 4b6837799976579b6bc469753c51cb964e49e7b0..470483cc0a713cbfc53d1a9e7db343a604918a2e 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -303,6 +303,7 @@ impl Render for ContextMenu { .w_full() .justify_between() .child(label_element) + .debug_selector(|| format!("MENU_ITEM-{}", label)) .children(action.as_ref().and_then(|action| { KeyBinding::for_action(&**action, cx) .map(|binding| div().ml_1().child(binding)) diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 55cdd93a5bee9d4be4c3f293a262b868b6b7825a..9d32073dbdb077614f1c23dda58ce0d4bae35ce8 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -1,9 +1,9 @@ use std::{cell::RefCell, rc::Rc}; use gpui::{ - overlay, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, Element, ElementId, - IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, - View, VisualContext, WindowContext, + overlay, AnchorCorner, AnyElement, BorrowWindow, Bounds, DismissEvent, DispatchPhase, Element, + ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseButton, MouseDownEvent, + ParentElement, Pixels, Point, View, VisualContext, WindowContext, }; pub struct RightClickMenu { @@ -136,10 +136,14 @@ impl Element for RightClickMenu { let child_layout_id = element_state.child_layout_id.clone(); let child_bounds = cx.layout_bounds(child_layout_id.unwrap()); + let interactive_bounds = InteractiveBounds { + bounds: bounds.intersect(&cx.content_mask().bounds), + stacking_order: cx.stacking_order().clone(), + }; cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble && event.button == MouseButton::Right - && bounds.contains(&event.position) + && interactive_bounds.visibly_contains(&event.position, cx) { cx.stop_propagation(); cx.prevent_default(); diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs index ade939fdaabcddfcdc6ac0a22b0222ff4f2b4170..7f1fcca721b9524b8879f2c6051e7481dc8db6ab 100644 --- a/crates/ui/src/components/tab.rs +++ b/crates/ui/src/components/tab.rs @@ -37,8 +37,11 @@ pub struct Tab { impl Tab { pub fn new(id: impl Into) -> Self { + let id = id.into(); Self { - div: div().id(id), + div: div() + .id(id.clone()) + .debug_selector(|| format!("TAB-{}", id)), selected: false, position: TabPosition::First, close_side: TabCloseSide::End, From 6c2da0d25b7c987ed1015ff95daa6e9e8ea980cd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 18 Jan 2024 13:28:23 -0700 Subject: [PATCH 96/96] Revert "Store a z-index id per-layer (#4128)" This reverts commit 28a23372185c7a30c7790085a6a010c3cad6ad09, reversing changes made to e992f84735a2ca166a7388d4eca69a6b4e6ee212. --- crates/collab_ui/src/face_pile.rs | 2 +- crates/gpui/src/style.rs | 8 +-- crates/gpui/src/styled.rs | 2 +- crates/gpui/src/window.rs | 71 ++++++++++++------------- crates/storybook/src/stories/z_index.rs | 4 +- crates/ui/src/styles/elevation.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 7 files changed, 45 insertions(+), 46 deletions(-) diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index 31132b298148944a95cde28918c51bcf94c33766..fb6c59cc8079073acbb6b481e214b54619f9eea6 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -13,7 +13,7 @@ impl RenderOnce for FacePile { let isnt_last = ix < player_count - 1; div() - .z_index((player_count - ix) as u16) + .z_index((player_count - ix) as u8) .when(isnt_last, |div| div.neg_mr_1()) .child(player) }); diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index bfc36ef6b116f355be5911151a32831cbd73f362..095233280edefc0b11d85e3a4ee255f54c8da13d 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -110,7 +110,7 @@ pub struct Style { /// The mouse cursor style shown when the mouse pointer is over an element. pub mouse_cursor: Option, - pub z_index: Option, + pub z_index: Option, #[cfg(debug_assertions)] pub debug: bool, @@ -386,7 +386,7 @@ impl Style { let background_color = self.background.as_ref().and_then(Fill::color); if background_color.map_or(false, |color| !color.is_transparent()) { - cx.with_z_index(1, |cx| { + cx.with_z_index(0, |cx| { let mut border_color = background_color.unwrap_or_default(); border_color.a = 0.; cx.paint_quad(quad( @@ -399,12 +399,12 @@ impl Style { }); } - cx.with_z_index(2, |cx| { + cx.with_z_index(0, |cx| { continuation(cx); }); if self.is_border_visible() { - cx.with_z_index(3, |cx| { + cx.with_z_index(0, |cx| { let corner_radii = self.corner_radii.to_pixels(bounds.size, rem_size); let border_widths = self.border_widths.to_pixels(rem_size); let max_border_width = border_widths.max(); diff --git a/crates/gpui/src/styled.rs b/crates/gpui/src/styled.rs index e8800d1ce9dea7831e7c5ab186c96def45434d1b..0eba1771f52d47bde32f465a887e52547f3a89b2 100644 --- a/crates/gpui/src/styled.rs +++ b/crates/gpui/src/styled.rs @@ -12,7 +12,7 @@ pub trait Styled: Sized { gpui_macros::style_helpers!(); - fn z_index(mut self, z_index: u16) -> Self { + fn z_index(mut self, z_index: u8) -> Self { self.style().z_index = Some(z_index); self } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index acb07b9f914bc308aaa7e123e420c4a4512cb561..2329a5251ed010791e043bc08b70fb2e5e19d895 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -43,23 +43,30 @@ use std::{ }; use util::{post_inc, ResultExt}; -const ACTIVE_DRAG_Z_INDEX: u16 = 1; +const ACTIVE_DRAG_Z_INDEX: u8 = 1; /// A global stacking order, which is created by stacking successive z-index values. /// Each z-index will always be interpreted in the context of its parent z-index. -#[derive(Debug, Deref, DerefMut, Clone, Ord, PartialOrd, PartialEq, Eq, Default)] -pub struct StackingOrder(SmallVec<[StackingContext; 64]>); - -/// A single entry in a primitive's z-index stacking order -#[derive(Clone, Ord, PartialOrd, PartialEq, Eq, Default)] -pub struct StackingContext { - z_index: u16, - id: u16, +#[derive(Deref, DerefMut, Clone, Ord, PartialOrd, PartialEq, Eq, Default)] +pub struct StackingOrder { + #[deref] + #[deref_mut] + context_stack: SmallVec<[u8; 64]>, + id: u32, } -impl std::fmt::Debug for StackingContext { +impl std::fmt::Debug for StackingOrder { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{{{}.{}}} ", self.z_index, self.id) + let mut stacks = self.context_stack.iter().peekable(); + write!(f, "[({}): ", self.id)?; + while let Some(z_index) = stacks.next() { + write!(f, "{z_index}")?; + if stacks.peek().is_some() { + write!(f, "->")?; + } + } + write!(f, "]")?; + Ok(()) } } @@ -308,8 +315,8 @@ pub(crate) struct Frame { pub(crate) scene: Scene, pub(crate) depth_map: Vec<(StackingOrder, EntityId, Bounds)>, pub(crate) z_index_stack: StackingOrder, - next_stacking_order_id: u16, - next_root_z_index: u16, + pub(crate) next_stacking_order_id: u32, + next_root_z_index: u8, content_mask_stack: Vec>, element_offset_stack: Vec>, requested_input_handler: Option, @@ -1104,11 +1111,7 @@ impl<'a> WindowContext<'a> { if level >= opaque_level { break; } - if opaque_level - .first() - .map(|c| c.z_index == ACTIVE_DRAG_Z_INDEX) - .unwrap_or(false) - { + if opaque_level.starts_with(&[ACTIVE_DRAG_Z_INDEX]) { continue; } @@ -2455,40 +2458,36 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { size: self.window().viewport_size, }, }; - - let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index); let new_stacking_order_id = post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); - let new_context = StackingContext { - z_index: new_root_z_index, - id: new_stacking_order_id, - }; - + let new_root_z_index = post_inc(&mut self.window_mut().next_frame.next_root_z_index); let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack); - - self.window_mut().next_frame.z_index_stack.push(new_context); + self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; + self.window_mut() + .next_frame + .z_index_stack + .push(new_root_z_index); self.window_mut().next_frame.content_mask_stack.push(mask); let result = f(self); self.window_mut().next_frame.content_mask_stack.pop(); self.window_mut().next_frame.z_index_stack = old_stacking_order; - result } /// Called during painting to invoke the given closure in a new stacking context. The given /// z-index is interpreted relative to the previous call to `stack`. - fn with_z_index(&mut self, z_index: u16, f: impl FnOnce(&mut Self) -> R) -> R { + fn with_z_index(&mut self, z_index: u8, f: impl FnOnce(&mut Self) -> R) -> R { let new_stacking_order_id = post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); - let new_context = StackingContext { - z_index, - id: new_stacking_order_id, - }; - - self.window_mut().next_frame.z_index_stack.push(new_context); + let old_stacking_order_id = mem::replace( + &mut self.window_mut().next_frame.z_index_stack.id, + new_stacking_order_id, + ); + self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; + self.window_mut().next_frame.z_index_stack.push(z_index); let result = f(self); + self.window_mut().next_frame.z_index_stack.id = old_stacking_order_id; self.window_mut().next_frame.z_index_stack.pop(); - result } diff --git a/crates/storybook/src/stories/z_index.rs b/crates/storybook/src/stories/z_index.rs index 63ee1af7591ee8a62f07f052b320db8a70077b9d..b6e49bfae32b046242e96a5de34d30c3be806b94 100644 --- a/crates/storybook/src/stories/z_index.rs +++ b/crates/storybook/src/stories/z_index.rs @@ -76,7 +76,7 @@ impl Styles for Div {} #[derive(IntoElement)] struct ZIndexExample { - z_index: u16, + z_index: u8, } impl RenderOnce for ZIndexExample { @@ -166,7 +166,7 @@ impl RenderOnce for ZIndexExample { } impl ZIndexExample { - pub fn new(z_index: u16) -> Self { + pub fn new(z_index: u8) -> Self { Self { z_index } } } diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index c2605fd152df49d04db57d6784bc3a54aaefd80d..0aa3786a279242c8a3a301b497a60ab0c2c537ec 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -20,7 +20,7 @@ pub enum ElevationIndex { } impl ElevationIndex { - pub fn z_index(self) -> u16 { + pub fn z_index(self) -> u8 { match self { ElevationIndex::Background => 0, ElevationIndex::Surface => 42, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a8aaa403fbf5d4cc11af3abe10f567f049138035..20c8bfc94a8adffb1eeb680d95c4f6dc602b34f5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4334,7 +4334,7 @@ impl Element for DisconnectedOverlay { } fn paint(&mut self, bounds: Bounds, overlay: &mut Self::State, cx: &mut WindowContext) { - cx.with_z_index(u16::MAX, |cx| { + cx.with_z_index(u8::MAX, |cx| { cx.add_opaque_layer(bounds); overlay.paint(cx); })