Compare commits
1018 Commits
v0.115.3-p
...
centralize
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e931a1d295 | ||
|
|
66f833eccf | ||
|
|
407478d411 | ||
|
|
a9b1273e2b | ||
|
|
1c3698ae20 | ||
|
|
4e07167288 | ||
|
|
80b6922de7 | ||
|
|
a4bfd0147d | ||
|
|
a3bab25792 | ||
|
|
ef39382c84 | ||
|
|
961f989388 | ||
|
|
e08fc0bbc0 | ||
|
|
51396344a3 | ||
|
|
e778e17f54 | ||
|
|
616ea131af | ||
|
|
b797cd3e71 | ||
|
|
5d95e13cc8 | ||
|
|
e655d2434a | ||
|
|
d8eea949ef | ||
|
|
a1085184a1 | ||
|
|
cf037ea4a8 | ||
|
|
d8a8feb45c | ||
|
|
3e6b84a726 | ||
|
|
79653d2175 | ||
|
|
ae313ff830 | ||
|
|
30b01b9bc0 | ||
|
|
286f654517 | ||
|
|
cc107f72f8 | ||
|
|
57efaa92cf | ||
|
|
dd84993d76 | ||
|
|
42a02e4fb6 | ||
|
|
2c402f9b5d | ||
|
|
49502af4d3 | ||
|
|
91b65958a7 | ||
|
|
b30fd3f574 | ||
|
|
1bb85c1b9a | ||
|
|
afbc61a344 | ||
|
|
02e53025f3 | ||
|
|
6a0740fa6d | ||
|
|
ae32706cfe | ||
|
|
ba0d7e35bb | ||
|
|
4b74f30d0a | ||
|
|
2979eb9da9 | ||
|
|
b88370d5ad | ||
|
|
ee8e1454fc | ||
|
|
e6f3731efd | ||
|
|
91f1be213b | ||
|
|
912f7e6c1a | ||
|
|
734bbfa66f | ||
|
|
64ceb61aa3 | ||
|
|
6da0fa98e2 | ||
|
|
a286ec4650 | ||
|
|
9785481aba | ||
|
|
9cd8e99a9a | ||
|
|
2ae39b70c8 | ||
|
|
4fdf6a867a | ||
|
|
c9893ce2fd | ||
|
|
d058515b81 | ||
|
|
0000e68310 | ||
|
|
37efe82c5e | ||
|
|
98f5a3d544 | ||
|
|
0aa690f915 | ||
|
|
f143467478 | ||
|
|
9d014b0f9a | ||
|
|
89b231afeb | ||
|
|
cef9aa3590 | ||
|
|
eeaa1e5cf9 | ||
|
|
d72cd3795a | ||
|
|
5900b815e1 | ||
|
|
5c7de103a9 | ||
|
|
196df1c378 | ||
|
|
4707248714 | ||
|
|
a623929340 | ||
|
|
da7628cc1e | ||
|
|
b46f5be490 | ||
|
|
7cd6825eee | ||
|
|
02bcc7bbeb | ||
|
|
488073deba | ||
|
|
b4042feccd | ||
|
|
11e657803e | ||
|
|
7e21e0f0eb | ||
|
|
4e544545d1 | ||
|
|
5a4e2e6b90 | ||
|
|
29c6061a14 | ||
|
|
b5ae2f060b | ||
|
|
a4cdf365be | ||
|
|
54eb452144 | ||
|
|
5447c6d5ee | ||
|
|
c8e918690f | ||
|
|
149a718d65 | ||
|
|
ee77728663 | ||
|
|
8efc38d43a | ||
|
|
170e332417 | ||
|
|
0ea59d6466 | ||
|
|
a5e037befa | ||
|
|
e3e988c150 | ||
|
|
4eb609a954 | ||
|
|
7af0edf5b6 | ||
|
|
e71f2b49e8 | ||
|
|
b8cdcf4e93 | ||
|
|
3781626379 | ||
|
|
3c3e5ed7d8 | ||
|
|
2fe9a1df21 | ||
|
|
4c3ec0a8e5 | ||
|
|
a35fdf45fc | ||
|
|
9a8225c855 | ||
|
|
a4c123bb11 | ||
|
|
b1a61ca21e | ||
|
|
793381f455 | ||
|
|
487edcb12b | ||
|
|
dc7f4f4786 | ||
|
|
5c060ceb1c | ||
|
|
5417127902 | ||
|
|
d0dbf8e1e2 | ||
|
|
a40f04b71f | ||
|
|
b4135dd2f1 | ||
|
|
47fc03ab98 | ||
|
|
a65688abac | ||
|
|
a2852e36ce | ||
|
|
0a57171066 | ||
|
|
4cb0f60779 | ||
|
|
83525bf142 | ||
|
|
b478a4c4d5 | ||
|
|
d13c1486da | ||
|
|
6c10ff8548 | ||
|
|
3993b0acde | ||
|
|
6345e6d4d2 | ||
|
|
d099d35948 | ||
|
|
47eaf1abd8 | ||
|
|
50a44dd8ba | ||
|
|
2b4f779918 | ||
|
|
f459fc5e27 | ||
|
|
62ea58618c | ||
|
|
3d9e051b07 | ||
|
|
be73dd852d | ||
|
|
e1ca8e81bb | ||
|
|
ff3f4f3027 | ||
|
|
2b3d9deabe | ||
|
|
83b55de73e | ||
|
|
4bfe46f53a | ||
|
|
31ff7d40ed | ||
|
|
a6403aad1a | ||
|
|
62d655183b | ||
|
|
de523c2d80 | ||
|
|
4be84f3db0 | ||
|
|
70a14bb22f | ||
|
|
d4e09230cc | ||
|
|
7e1d61d116 | ||
|
|
46bd6088e8 | ||
|
|
ff4da878f9 | ||
|
|
fbcaf96ab5 | ||
|
|
6973b1b592 | ||
|
|
ead5206741 | ||
|
|
b52db696d3 | ||
|
|
52b9fc303b | ||
|
|
c27dd57a48 | ||
|
|
b95cc7221c | ||
|
|
05cb68eff1 | ||
|
|
842f15c65b | ||
|
|
82c8b49dac | ||
|
|
8418f25d0a | ||
|
|
d6383ab0c6 | ||
|
|
0b4b81fa0c | ||
|
|
ad8165ae79 | ||
|
|
f4a954db4f | ||
|
|
8b4cf38379 | ||
|
|
02606d1fb9 | ||
|
|
bc3e6649f8 | ||
|
|
4c63c74f92 | ||
|
|
e4f9bddbab | ||
|
|
139fe7c1f1 | ||
|
|
936c78be94 | ||
|
|
fcbd58fed4 | ||
|
|
3cf003763e | ||
|
|
2b278e69f7 | ||
|
|
292fac37bb | ||
|
|
8bac4e199d | ||
|
|
6170895932 | ||
|
|
4e7005b4f7 | ||
|
|
b5e9e277db | ||
|
|
23d5f3f3e8 | ||
|
|
d13a21c238 | ||
|
|
bbbdb9ff3e | ||
|
|
63c3edfb83 | ||
|
|
8d994ce8c5 | ||
|
|
6b06bb4ffe | ||
|
|
8791f7cefc | ||
|
|
fb3382bcc5 | ||
|
|
35838ae554 | ||
|
|
0dd6c50a20 | ||
|
|
c166311c06 | ||
|
|
2484a6969a | ||
|
|
c041799c6a | ||
|
|
2efd7cedbc | ||
|
|
ed098c834e | ||
|
|
3d1dae9a06 | ||
|
|
0d30b698a4 | ||
|
|
8a361c93e2 | ||
|
|
c6e44683e6 | ||
|
|
4e1b4c4390 | ||
|
|
d8cb0e8a2a | ||
|
|
1ae25f52a1 | ||
|
|
fd133df896 | ||
|
|
c2c7eead8a | ||
|
|
4ace342cf0 | ||
|
|
08418618ab | ||
|
|
17a80ca09b | ||
|
|
ac3622cc6a | ||
|
|
6f17cf7337 | ||
|
|
da960fffe7 | ||
|
|
9bce30687e | ||
|
|
cfe7336519 | ||
|
|
3ad967778a | ||
|
|
cd08d349a5 | ||
|
|
837b4c3462 | ||
|
|
4f196f66fc | ||
|
|
d9adfdeac2 | ||
|
|
435256ab0f | ||
|
|
0db62f0ceb | ||
|
|
53b1948c44 | ||
|
|
d88fc27b79 | ||
|
|
900a2bfb87 | ||
|
|
be2ad2ff86 | ||
|
|
0dd84924e4 | ||
|
|
052e05fc74 | ||
|
|
7ba7b925ff | ||
|
|
ceede28fab | ||
|
|
474f09ca3f | ||
|
|
c863227dc2 | ||
|
|
057b235c56 | ||
|
|
15f62a49f7 | ||
|
|
4f32f66271 | ||
|
|
9059d70153 | ||
|
|
93029376d9 | ||
|
|
a4024b297e | ||
|
|
04389939d3 | ||
|
|
416bb45531 | ||
|
|
137e4e9251 | ||
|
|
d59de96921 | ||
|
|
e2bfd46455 | ||
|
|
1e4a7e6ef1 | ||
|
|
7899833367 | ||
|
|
1ad1cc1148 | ||
|
|
aa55e55c7a | ||
|
|
985d4c7429 | ||
|
|
a807e798ec | ||
|
|
26a31b41b9 | ||
|
|
bfbbec0b01 | ||
|
|
3094cb749e | ||
|
|
426d298173 | ||
|
|
943acc5819 | ||
|
|
ee509e043d | ||
|
|
06b9055e27 | ||
|
|
72eef116c9 | ||
|
|
a91a42763f | ||
|
|
48faa171b5 | ||
|
|
ab8d0abbc1 | ||
|
|
a874a96e76 | ||
|
|
70c6660ae4 | ||
|
|
f9e7c79672 | ||
|
|
5b3b15e95c | ||
|
|
85a1a8f777 | ||
|
|
ce1489f5dc | ||
|
|
0e19da3107 | ||
|
|
9ff73d3a0a | ||
|
|
5c8257585a | ||
|
|
9a7de98242 | ||
|
|
fd428dfa66 | ||
|
|
61ab1834c7 | ||
|
|
07a266d93f | ||
|
|
b2965469e1 | ||
|
|
1055f59925 | ||
|
|
cfc050e3fe | ||
|
|
599f81f345 | ||
|
|
6147530156 | ||
|
|
a71365a1d3 | ||
|
|
988fb91a61 | ||
|
|
226d4929b5 | ||
|
|
5730d0ef21 | ||
|
|
9cd55a8806 | ||
|
|
d4e088cb11 | ||
|
|
2a714d6dd8 | ||
|
|
801125974a | ||
|
|
b9023a3e49 | ||
|
|
a13468e6ad | ||
|
|
13c064186a | ||
|
|
d147db1db9 | ||
|
|
5018072203 | ||
|
|
19e842b860 | ||
|
|
e91ecec8cd | ||
|
|
a055a4c163 | ||
|
|
fa36adbf1f | ||
|
|
979db25e91 | ||
|
|
a2f0accb74 | ||
|
|
c94c6614ed | ||
|
|
630997db28 | ||
|
|
b65129437f | ||
|
|
1a86e4ff96 | ||
|
|
a9349267ec | ||
|
|
573377d9f9 | ||
|
|
a656d11fed | ||
|
|
e09b07ddae | ||
|
|
6362221363 | ||
|
|
a579713a45 | ||
|
|
a4a501603e | ||
|
|
2e00da5a79 | ||
|
|
5f5b86ee24 | ||
|
|
29413bc72c | ||
|
|
a63b4c598c | ||
|
|
3088cbea68 | ||
|
|
c43969eaed | ||
|
|
51ceb52931 | ||
|
|
c9aa4a0e00 | ||
|
|
e489e2e583 | ||
|
|
01d4e711d8 | ||
|
|
d3eff6371e | ||
|
|
d2feaa41a5 | ||
|
|
31a4892a55 | ||
|
|
f5ca514bf0 | ||
|
|
2a22208518 | ||
|
|
b3e1514b00 | ||
|
|
52e72d9648 | ||
|
|
e3ed3b8c1c | ||
|
|
fd6320b136 | ||
|
|
e53b9f5ccf | ||
|
|
35670368a5 | ||
|
|
44d40625fe | ||
|
|
cc97b04627 | ||
|
|
a334a21f3e | ||
|
|
c0846d6f74 | ||
|
|
13f9fec0bd | ||
|
|
be57059195 | ||
|
|
b87c45e6f5 | ||
|
|
4684440202 | ||
|
|
0140bb862e | ||
|
|
ca8e8d1065 | ||
|
|
9f21fe2d50 | ||
|
|
97eae4b081 | ||
|
|
6159a59534 | ||
|
|
2cde1a2e15 | ||
|
|
1c437a2b92 | ||
|
|
717b2885f8 | ||
|
|
43b8d65fee | ||
|
|
137104e00e | ||
|
|
706dd8246e | ||
|
|
b871f906d6 | ||
|
|
385c830bef | ||
|
|
d9b0828beb | ||
|
|
721d7615c3 | ||
|
|
f312c58b30 | ||
|
|
27d6432c84 | ||
|
|
e7094cc98d | ||
|
|
b503edf24f | ||
|
|
376716254f | ||
|
|
a9f817fc14 | ||
|
|
6f5b1064ee | ||
|
|
9cb6f6689e | ||
|
|
39a115b264 | ||
|
|
cbce49ff68 | ||
|
|
a208229a2c | ||
|
|
149e90c3d9 | ||
|
|
75c1f0e997 | ||
|
|
3779316e4e | ||
|
|
805df4fd15 | ||
|
|
bb5fa6bc13 | ||
|
|
3120a9cbea | ||
|
|
2d59492aac | ||
|
|
e37173fe97 | ||
|
|
3baf125f4a | ||
|
|
050ff6b27d | ||
|
|
d12eb0581a | ||
|
|
1e89092d62 | ||
|
|
303189e086 | ||
|
|
e271bbe896 | ||
|
|
dfd1b5aa11 | ||
|
|
aa27e1bcec | ||
|
|
927d18b0f3 | ||
|
|
cb8109ab98 | ||
|
|
4942b1962b | ||
|
|
f03c0f6e63 | ||
|
|
d1805d8ada | ||
|
|
f12510b8b0 | ||
|
|
41f55e4d0d | ||
|
|
fdcb413e33 | ||
|
|
f02a3e8c68 | ||
|
|
fbfe108317 | ||
|
|
fcbc18a380 | ||
|
|
9b94f1483a | ||
|
|
906505f4dc | ||
|
|
f1ebad22db | ||
|
|
f35453caad | ||
|
|
7f4d03fab2 | ||
|
|
22e99033e1 | ||
|
|
3198eb6c6d | ||
|
|
9a36402fbe | ||
|
|
10b015cdf0 | ||
|
|
cd2abc7c1d | ||
|
|
8ed642dc16 | ||
|
|
fe46459982 | ||
|
|
6add17338c | ||
|
|
3271cf1030 | ||
|
|
89884d289f | ||
|
|
dd42adc4e5 | ||
|
|
b9bbc4a698 | ||
|
|
af7c93b8d5 | ||
|
|
c97c9f98c0 | ||
|
|
76cea3e7e8 | ||
|
|
ea9a42b802 | ||
|
|
59d0feafc5 | ||
|
|
b8f9918f75 | ||
|
|
b66b4915cf | ||
|
|
6792a216a6 | ||
|
|
d678efe7dd | ||
|
|
bc130fd6b0 | ||
|
|
01ecbec2b0 | ||
|
|
ddbd5cf2cb | ||
|
|
40c1ef88e2 | ||
|
|
0b7072bf67 | ||
|
|
9f1e7a1e21 | ||
|
|
d4a246cae8 | ||
|
|
3c49011d08 | ||
|
|
7330495709 | ||
|
|
3f87c3541f | ||
|
|
66e0650f75 | ||
|
|
db39c3d582 | ||
|
|
fdc0ef8ce0 | ||
|
|
b4fc8ea6f7 | ||
|
|
3c56f6d5c1 | ||
|
|
38d844ab66 | ||
|
|
5768f14a03 | ||
|
|
e7c14dacae | ||
|
|
1599ee5467 | ||
|
|
fe315c6111 | ||
|
|
36d2ab4537 | ||
|
|
cdf7c10b57 | ||
|
|
da44b51a53 | ||
|
|
55374e8ac0 | ||
|
|
3694265b6b | ||
|
|
e3fc810b3d | ||
|
|
db8e58b888 | ||
|
|
e3c0b1f903 | ||
|
|
03df63aa00 | ||
|
|
cea26c9d10 | ||
|
|
c5367e3085 | ||
|
|
9ce7395b74 | ||
|
|
ff45b43857 | ||
|
|
62d6a85a71 | ||
|
|
4290c67b6a | ||
|
|
cbbb0d033e | ||
|
|
7b722c326f | ||
|
|
188d727d31 | ||
|
|
6b58eb202d | ||
|
|
0be58eb61a | ||
|
|
b09d2219d0 | ||
|
|
33c41fb5e1 | ||
|
|
0ee61e214d | ||
|
|
4adafb1148 | ||
|
|
a4ea7bf928 | ||
|
|
f3b764b7d2 | ||
|
|
e4bc346723 | ||
|
|
fd34d1da31 | ||
|
|
9abf5a1bf2 | ||
|
|
049314e84e | ||
|
|
d03e29d55d | ||
|
|
8feb11ccee | ||
|
|
008c56a09b | ||
|
|
cf42d95af6 | ||
|
|
60e4311294 | ||
|
|
cbbcc3502a | ||
|
|
44890d6966 | ||
|
|
2517b1017b | ||
|
|
1fecd3c327 | ||
|
|
be636348c4 | ||
|
|
13dad89a85 | ||
|
|
324aeaa0de | ||
|
|
ad05f22256 | ||
|
|
2ef3954700 | ||
|
|
650ca1f39b | ||
|
|
fce0c1f4d0 | ||
|
|
e9730e0b5c | ||
|
|
cd93ac1d2a | ||
|
|
c739906413 | ||
|
|
c7d8169cab | ||
|
|
213ed2028c | ||
|
|
6955579f19 | ||
|
|
113c7287df | ||
|
|
4353bdb9d5 | ||
|
|
40b219f861 | ||
|
|
f67d4cea31 | ||
|
|
0dc02b8354 | ||
|
|
133b1a8ad2 | ||
|
|
f0cc54a0b5 | ||
|
|
27501d2929 | ||
|
|
77c8108f9b | ||
|
|
1c850f495c | ||
|
|
1d35a815a6 | ||
|
|
79e6dedb7a | ||
|
|
32837d67be | ||
|
|
c653a02be3 | ||
|
|
f4c93abad5 | ||
|
|
62155f3a88 | ||
|
|
fdde76c1a5 | ||
|
|
ab5b76e943 | ||
|
|
be6c909587 | ||
|
|
a5a0ad8b5c | ||
|
|
726d761646 | ||
|
|
9ac9532d3d | ||
|
|
63cc9e5068 | ||
|
|
0cab3de0ae | ||
|
|
21476ec51d | ||
|
|
c51cedef14 | ||
|
|
7964b35607 | ||
|
|
8987b2205c | ||
|
|
970c7b8987 | ||
|
|
a283cbaf8f | ||
|
|
0b2aa052ea | ||
|
|
52e4c577d2 | ||
|
|
4596e7a68a | ||
|
|
b66e1d2d58 | ||
|
|
6f064cfc36 | ||
|
|
260a753005 | ||
|
|
6634a5e9f6 | ||
|
|
1b6721170a | ||
|
|
63ce7cd407 | ||
|
|
40a95221ea | ||
|
|
a8a5b9524d | ||
|
|
53d77b192a | ||
|
|
9b0bea32ed | ||
|
|
28dfd3ab43 | ||
|
|
53ff5ff724 | ||
|
|
d0a673ec62 | ||
|
|
7a9f764aa0 | ||
|
|
5b96ffbbd1 | ||
|
|
efb4ff816a | ||
|
|
1f51f74670 | ||
|
|
a88372dc99 | ||
|
|
f272881a6b | ||
|
|
25df11dd26 | ||
|
|
89f62a1a69 | ||
|
|
f9d569f1b8 | ||
|
|
808a0626c0 | ||
|
|
c7e19c0bcd | ||
|
|
0b78a401de | ||
|
|
58d8c51255 | ||
|
|
5e3d0a6d03 | ||
|
|
aa34637705 | ||
|
|
901bd6ac87 | ||
|
|
445f0da4d8 | ||
|
|
d1b47b4059 | ||
|
|
f3cbce7e86 | ||
|
|
04d019ef66 | ||
|
|
d23bba5b18 | ||
|
|
282e4398a0 | ||
|
|
fa223e0c6f | ||
|
|
6a164db700 | ||
|
|
8ed04fe798 | ||
|
|
853daf953b | ||
|
|
8b9b19195d | ||
|
|
5c3b8a1af1 | ||
|
|
0ee4ad6ba0 | ||
|
|
ef4bc5e20b | ||
|
|
439df24526 | ||
|
|
1fcd006b2a | ||
|
|
7c19650a40 | ||
|
|
393be3cedf | ||
|
|
c91207b450 | ||
|
|
7ad6d489a4 | ||
|
|
794b795800 | ||
|
|
1bf94f0251 | ||
|
|
2461902086 | ||
|
|
e9c40963ab | ||
|
|
08c4e1abdc | ||
|
|
5e5eb25aab | ||
|
|
ed5c05b272 | ||
|
|
b04838c23a | ||
|
|
49d30b4f67 | ||
|
|
b9a904e552 | ||
|
|
6c653b9c05 | ||
|
|
b692b4c9c2 | ||
|
|
f829120f51 | ||
|
|
2a82dff2fe | ||
|
|
646b74e0a7 | ||
|
|
75324abbb5 | ||
|
|
0c23e6738b | ||
|
|
ea708c50f0 | ||
|
|
8839bfa1df | ||
|
|
2c2c633104 | ||
|
|
18393d1fac | ||
|
|
7842fdeb32 | ||
|
|
45f7ab876f | ||
|
|
cbd1ce64f7 | ||
|
|
9a916d71b0 | ||
|
|
a40515dbb3 | ||
|
|
edb2f60d9d | ||
|
|
02bd4fb1f1 | ||
|
|
e96197d63b | ||
|
|
d02ff42608 | ||
|
|
f798b193d0 | ||
|
|
197f355729 | ||
|
|
7a1eb54106 | ||
|
|
42a3da4ba3 | ||
|
|
84aaeb4360 | ||
|
|
da3ba35d1c | ||
|
|
2a68a6171a | ||
|
|
fffe4f51fb | ||
|
|
27e1787ce3 | ||
|
|
9e6103f863 | ||
|
|
d2362d7f12 | ||
|
|
79e0d8ce3b | ||
|
|
ee45db8a78 | ||
|
|
8fc15c05c5 | ||
|
|
256f0308ae | ||
|
|
d62b151bc0 | ||
|
|
b2a0c32148 | ||
|
|
668f5accab | ||
|
|
66b93212fe | ||
|
|
57a9f024e7 | ||
|
|
22cd62213b | ||
|
|
bbe9986f38 | ||
|
|
1abc9800af | ||
|
|
034c11cc96 | ||
|
|
0885fa67fc | ||
|
|
c8cb1140b9 | ||
|
|
c092cfbfb3 | ||
|
|
a4b271e063 | ||
|
|
110612bf26 | ||
|
|
fd31e429f5 | ||
|
|
12e7f61f62 | ||
|
|
1a747c8457 | ||
|
|
2d18b949ad | ||
|
|
46c998ca8d | ||
|
|
a8d96790cf | ||
|
|
b72c54fc31 | ||
|
|
89c8a7c242 | ||
|
|
41fc30f62e | ||
|
|
f4c7b13397 | ||
|
|
f7c995c4a0 | ||
|
|
cd818f580c | ||
|
|
dba94c5122 | ||
|
|
dbb501d7eb | ||
|
|
d711087529 | ||
|
|
ebddb612c8 | ||
|
|
62c41e226b | ||
|
|
b94c335605 | ||
|
|
6bbb1642b8 | ||
|
|
e9dcca7712 | ||
|
|
1ec81e02da | ||
|
|
bcdefb8ec8 | ||
|
|
5644815c4c | ||
|
|
9e1d797445 | ||
|
|
ec798e6574 | ||
|
|
d7473ad6e7 | ||
|
|
5ebf1d9e4f | ||
|
|
f76e1cfd91 | ||
|
|
489c25ac6a | ||
|
|
ac07e230fa | ||
|
|
147c99f1a7 | ||
|
|
02a6a2e1a3 | ||
|
|
4863c9ac25 | ||
|
|
2c350a71fe | ||
|
|
fdd64832e7 | ||
|
|
eff925cb6a | ||
|
|
7e2ff63270 | ||
|
|
cc9eff89f5 | ||
|
|
e4884f1d76 | ||
|
|
02e7a06cbb | ||
|
|
5ae2060783 | ||
|
|
14def2a1a3 | ||
|
|
7c9e2f6b7d | ||
|
|
f6a7a6c4d4 | ||
|
|
2aee3e3192 | ||
|
|
5e558e2a58 | ||
|
|
80f315106d | ||
|
|
80c8fd1f4c | ||
|
|
886ec79d58 | ||
|
|
c8ddc95caa | ||
|
|
8f1c74b8bc | ||
|
|
0ced310f93 | ||
|
|
74e3b12a26 | ||
|
|
cf971f706c | ||
|
|
b5c790ed29 | ||
|
|
6549a9a091 | ||
|
|
3b8c566f31 | ||
|
|
f833cd7c16 | ||
|
|
e9002ab10a | ||
|
|
a1c8f01ff3 | ||
|
|
54ea7257ff | ||
|
|
d09dfe01f5 | ||
|
|
3c1bf0260c | ||
|
|
cb0880cf3c | ||
|
|
a58f393458 | ||
|
|
0ef97edd6e | ||
|
|
1f538c5fdd | ||
|
|
5f172a52a4 | ||
|
|
1b0ec82caa | ||
|
|
7998e8281c | ||
|
|
3f9fe58c48 | ||
|
|
9eb98122ec | ||
|
|
1de02cf6e5 | ||
|
|
b29cea287b | ||
|
|
735f2029e9 | ||
|
|
5660c8f655 | ||
|
|
ed31d36ac1 | ||
|
|
d8757845a9 | ||
|
|
2ee0ecb677 | ||
|
|
d2fe9f8f9b | ||
|
|
c9dd6b879c | ||
|
|
65bb05af4c | ||
|
|
82534b6612 | ||
|
|
79567d1c87 | ||
|
|
5b6fd3ac1d | ||
|
|
a27db35ff7 | ||
|
|
02e507b973 | ||
|
|
f2faa70f73 | ||
|
|
863222edc5 | ||
|
|
fdc2d7ce42 | ||
|
|
dec4cfa0b4 | ||
|
|
78e1c0f9c3 | ||
|
|
631e264e3c | ||
|
|
ab140ee4c2 | ||
|
|
fc9b662042 | ||
|
|
8141f4fd86 | ||
|
|
e0ca7f844a | ||
|
|
4c4b235b13 | ||
|
|
dc7e4a4b17 | ||
|
|
27703a3279 | ||
|
|
7b4b068230 | ||
|
|
ceb5d0c69e | ||
|
|
38d41acf9b | ||
|
|
d86da04584 | ||
|
|
5e79807f6f | ||
|
|
a80b145d0d | ||
|
|
ce8bdb354b | ||
|
|
dccdcd3221 | ||
|
|
8d57d6ca6f | ||
|
|
e534c5fdcd | ||
|
|
eed5a698cf | ||
|
|
412c6157b1 | ||
|
|
dffe0ea058 | ||
|
|
2299730538 | ||
|
|
ede86d9187 | ||
|
|
c9b50c8bab | ||
|
|
7c5df51d2e | ||
|
|
30725d6a17 | ||
|
|
09db455db2 | ||
|
|
c2d3e74ffb | ||
|
|
62bfa3b452 | ||
|
|
16b5d4b35c | ||
|
|
20352c51c1 | ||
|
|
f9efaebddf | ||
|
|
af72772a72 | ||
|
|
b73ccc8180 | ||
|
|
d433da1e70 | ||
|
|
001ce47a0c | ||
|
|
e5616bce98 | ||
|
|
92fbdb429c | ||
|
|
b43dc480a9 | ||
|
|
2c2c14a360 | ||
|
|
dffe323e73 | ||
|
|
f0ca7141b8 | ||
|
|
1f6c69c7dc | ||
|
|
590238bcca | ||
|
|
caa5fccbc4 | ||
|
|
1dd6625dd4 | ||
|
|
ee695bbb34 | ||
|
|
9695ea1017 | ||
|
|
e0ec5032e9 | ||
|
|
948c065f86 | ||
|
|
ae6ddceb67 | ||
|
|
eff3a72fb5 | ||
|
|
591dc9d82a | ||
|
|
71a1125e88 | ||
|
|
959b2961ff | ||
|
|
45992b0d63 | ||
|
|
9162f299a7 | ||
|
|
2c5603032d | ||
|
|
63667ecf6f | ||
|
|
13bb16577c | ||
|
|
1c9b984738 | ||
|
|
7db0a9e105 | ||
|
|
fc16e4509a | ||
|
|
c82fea375d | ||
|
|
5bdaf0e074 | ||
|
|
c10d8a8110 | ||
|
|
79773178c8 | ||
|
|
2c2e5144c9 | ||
|
|
059959a4ab | ||
|
|
3627ff87f0 | ||
|
|
9a2fd18425 | ||
|
|
4cb4033a36 | ||
|
|
b212aab00d | ||
|
|
584a3a7627 | ||
|
|
63213b5a57 | ||
|
|
4a5f703c32 | ||
|
|
a3e4559b0e | ||
|
|
26c797c358 | ||
|
|
fb352af35c | ||
|
|
4f4029524f | ||
|
|
e928ed44a0 | ||
|
|
23626aa902 | ||
|
|
5ab6874ae9 | ||
|
|
4ee4e4e8d8 | ||
|
|
7a0aa1e5df | ||
|
|
b6ed3b258c | ||
|
|
50fe683305 | ||
|
|
b5924d6b11 | ||
|
|
80ae640060 | ||
|
|
0af0c5549c | ||
|
|
68d309e79c | ||
|
|
fd2f1c2594 | ||
|
|
2108ddf621 | ||
|
|
ee23fa43dc | ||
|
|
118c9b5fe8 | ||
|
|
b9a917f42a | ||
|
|
7dc22fef24 | ||
|
|
24b08921fe | ||
|
|
dfe4fc4d0a | ||
|
|
11c16258eb | ||
|
|
ff734d494f | ||
|
|
3b1a0652ae | ||
|
|
b3e741b397 | ||
|
|
1a5f6f604b | ||
|
|
0f7fc8c1a0 | ||
|
|
9ffe78d264 | ||
|
|
45230dcaf3 | ||
|
|
aa3c9b8568 | ||
|
|
859f2d2862 | ||
|
|
63b65b2b2e | ||
|
|
1da18ebe9a | ||
|
|
6426997abb | ||
|
|
6a5b5f022d | ||
|
|
2de6758443 | ||
|
|
fe83919746 | ||
|
|
6730ff3624 | ||
|
|
b0f9144167 | ||
|
|
0edd89a92f | ||
|
|
6f7995c150 | ||
|
|
379ba620b2 | ||
|
|
2bf48872b6 | ||
|
|
03ebf0a5a9 | ||
|
|
26ae31b503 | ||
|
|
6b0457c52f | ||
|
|
d81fb3680e | ||
|
|
d788bf3f1e | ||
|
|
c07455efa7 | ||
|
|
d85db44552 | ||
|
|
89aa6a3726 | ||
|
|
f5679f98d6 | ||
|
|
be509a5ce0 | ||
|
|
5dd9db3607 | ||
|
|
aace0d816d | ||
|
|
4479628342 | ||
|
|
0a1765b01f | ||
|
|
c3e7732eab | ||
|
|
a985b7aab4 | ||
|
|
20ae58eddd | ||
|
|
9408eecb6e | ||
|
|
2ab84b81da | ||
|
|
e821e1fc35 | ||
|
|
237efc841e | ||
|
|
4b23c5c658 | ||
|
|
1c52b936bc | ||
|
|
4f507756d6 | ||
|
|
12b58f5b60 | ||
|
|
f09d9ef723 | ||
|
|
be5eb3e3b5 | ||
|
|
53f3f960d2 | ||
|
|
6f9e08ceea | ||
|
|
85d72f63c1 | ||
|
|
4c4ec221af | ||
|
|
910aeb4f56 | ||
|
|
0d33f01fa4 | ||
|
|
e0ccaa60ff | ||
|
|
a40a5fb212 | ||
|
|
aed11ee8cb | ||
|
|
13f4cc563c | ||
|
|
ab75dbe7af | ||
|
|
88188b0c30 | ||
|
|
ce16e5b54a | ||
|
|
134f3eb79f | ||
|
|
5fefc121bf | ||
|
|
9fedb4c7ee | ||
|
|
f0c205be5d | ||
|
|
f0bc4a04bd | ||
|
|
1abc876c15 | ||
|
|
8e4f2fb25a | ||
|
|
faa896343b | ||
|
|
a8a5785ec5 | ||
|
|
c9be637b6b | ||
|
|
0b7e324d86 | ||
|
|
8c59a2d2c0 | ||
|
|
21d3ae603d | ||
|
|
1b07fd6a70 | ||
|
|
a75f1a4a95 | ||
|
|
eef6c3729e | ||
|
|
9849a0a6d8 | ||
|
|
6d62e6c562 | ||
|
|
0441b4805a | ||
|
|
e1c8369b3d | ||
|
|
4f74d65d0c | ||
|
|
ebbbeca9a6 | ||
|
|
cb11c1282c | ||
|
|
a003a91212 | ||
|
|
bac39e85a8 | ||
|
|
e228422f14 | ||
|
|
8b7be8f614 | ||
|
|
e5a5b1e84c | ||
|
|
5098fafa02 | ||
|
|
f987a640fd | ||
|
|
c63ca09eed | ||
|
|
164084c61c | ||
|
|
180ba42456 | ||
|
|
bd6fa66a7c | ||
|
|
16dc978bb4 | ||
|
|
f922ad9f7f | ||
|
|
39887be7d0 | ||
|
|
d516ae0d8a | ||
|
|
3ffcb97da2 | ||
|
|
e78538e162 | ||
|
|
865baaa1a1 | ||
|
|
5fdfdb046c | ||
|
|
5eb89781e3 | ||
|
|
b34b197d89 | ||
|
|
679851e349 | ||
|
|
8d4652a4db | ||
|
|
21dfe58ad9 | ||
|
|
75afb65b26 | ||
|
|
0a8a84f656 | ||
|
|
0e1597d385 | ||
|
|
428c517693 | ||
|
|
81098312ef | ||
|
|
5243401afa | ||
|
|
591299b4c7 | ||
|
|
d9b8c07b1e | ||
|
|
e20309f560 | ||
|
|
daf6201deb | ||
|
|
b64fe04803 | ||
|
|
82b3efa16c | ||
|
|
cdae8ebd06 | ||
|
|
1b39916438 | ||
|
|
091d93533d | ||
|
|
69bfd47cf9 | ||
|
|
7b76db4b50 | ||
|
|
74a5ae5633 | ||
|
|
fac029b808 | ||
|
|
f052329a6b | ||
|
|
d2c1897385 | ||
|
|
14993e0876 | ||
|
|
624ea58f2e | ||
|
|
cc0bc444b1 | ||
|
|
f9cd45269a | ||
|
|
02174084ca | ||
|
|
90b8acfb34 | ||
|
|
4fbbb28da2 | ||
|
|
54cfcef0aa | ||
|
|
a37f86f294 | ||
|
|
92d2048aa4 | ||
|
|
8638fc4149 | ||
|
|
012543052b | ||
|
|
1a1451a943 | ||
|
|
b4a5701e12 | ||
|
|
e49325080c | ||
|
|
c95a7c7387 | ||
|
|
bda1dc7721 | ||
|
|
9d53287341 | ||
|
|
481e42ade9 | ||
|
|
27a6761afc | ||
|
|
4c27f4453c | ||
|
|
04bbd107c1 | ||
|
|
b357ae4dc3 | ||
|
|
cd60d466b1 | ||
|
|
0eef4d03ed | ||
|
|
0b8f9067ff | ||
|
|
4f040208c6 | ||
|
|
9451bd605d | ||
|
|
3bdd51cb2a | ||
|
|
4ebae4d8bd | ||
|
|
df5de47a78 | ||
|
|
2bd428102b | ||
|
|
fc2251e7ed | ||
|
|
fb377aed73 | ||
|
|
d8ed7c0721 | ||
|
|
6ab3825ab1 | ||
|
|
87cf0cf5ac | ||
|
|
0d31722589 | ||
|
|
3c05d57e6a | ||
|
|
509504d4d2 | ||
|
|
139708d486 | ||
|
|
7e5aa5ce77 | ||
|
|
e77846d2dc | ||
|
|
e377bd805b | ||
|
|
6f5cc0af94 | ||
|
|
e99fa77bda | ||
|
|
41e7653906 | ||
|
|
c7b79c9aef | ||
|
|
390b0d8d56 | ||
|
|
ee260a5e24 | ||
|
|
960ef7116d | ||
|
|
3e2c517dd1 | ||
|
|
dbfc7d3555 | ||
|
|
c23a610d52 | ||
|
|
233aac5573 | ||
|
|
043cef89eb | ||
|
|
ac34229118 | ||
|
|
912c30c05b | ||
|
|
97e6fd295a | ||
|
|
a8bf0834e6 | ||
|
|
a4035f15af | ||
|
|
35481e2c79 | ||
|
|
5d59108b97 | ||
|
|
f735f5287e | ||
|
|
5fbc60d8da | ||
|
|
77acba9e4c | ||
|
|
6bf7ad71eb | ||
|
|
bcf449d3fe | ||
|
|
3ac545088a | ||
|
|
600b564bbf | ||
|
|
d551b41aae |
263
.github/workflows/ci.yml
vendored
@@ -1,144 +1,149 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
160
.github/workflows/release_nightly.yml
vendored
@@ -1,98 +1,98 @@
|
||||
name: Release Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
schedule:
|
||||
# Fire every day at 1:00pm and 1:00am
|
||||
- cron: "0 1,13 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
|
||||
535
Cargo.lock
generated
17
Cargo.toml
@@ -1,13 +1,16 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/activity_indicator2",
|
||||
"crates/ai",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
"crates/audio",
|
||||
"crates/audio2",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update2",
|
||||
"crates/breadcrumbs",
|
||||
"crates/breadcrumbs2",
|
||||
"crates/call",
|
||||
"crates/call2",
|
||||
"crates/channel",
|
||||
@@ -59,6 +62,7 @@ members = [
|
||||
"crates/language",
|
||||
"crates/language2",
|
||||
"crates/language_selector",
|
||||
"crates/language_selector2",
|
||||
"crates/language_tools",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
@@ -73,6 +77,7 @@ members = [
|
||||
"crates/notifications",
|
||||
"crates/notifications2",
|
||||
"crates/outline",
|
||||
"crates/outline2",
|
||||
"crates/picker",
|
||||
"crates/picker2",
|
||||
"crates/plugin",
|
||||
@@ -85,12 +90,17 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_panel2",
|
||||
"crates/project_symbols",
|
||||
"crates/project_symbols2",
|
||||
"crates/quick_action_bar2",
|
||||
"crates/recent_projects",
|
||||
"crates/recent_projects2",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rpc2",
|
||||
"crates/search",
|
||||
"crates/search2",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_index2",
|
||||
"crates/settings",
|
||||
"crates/settings2",
|
||||
"crates/snippet",
|
||||
@@ -110,10 +120,10 @@ members = [
|
||||
"crates/theme_selector2",
|
||||
"crates/ui2",
|
||||
"crates/util",
|
||||
"crates/semantic_index",
|
||||
"crates/story",
|
||||
"crates/vim",
|
||||
"crates/vcs_menu",
|
||||
"crates/vcs_menu2",
|
||||
"crates/workspace2",
|
||||
"crates/welcome",
|
||||
"crates/welcome2",
|
||||
@@ -129,6 +139,7 @@ resolver = "2"
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.57" }
|
||||
async-trait = { version = "0.1" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
# TODO: Switch back to the published version of `ctor` once:
|
||||
# 1. A new version of `ctor` is published with this change: https://github.com/mmastrac/rust-ctor/pull/295
|
||||
# 2. We've confirmed it's fine to update to the latest version of `ctor` (we're currently on v0.1.20).
|
||||
@@ -196,11 +207,11 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
|
||||
tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"}
|
||||
tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "b5f461a69bf3df7298b1903574d506179e6390b0" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
|
||||
1
assets/icons/copy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||
|
After Width: | Height: | Size: 338 B |
1
assets/icons/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/><path d="M9 18c-4.51 2-5-2-7-2"/></svg>
|
||||
|
After Width: | Height: | Size: 510 B |
@@ -1,4 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
|
||||
<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize-2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" x2="14" y1="3" y2="10"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 315 B After Width: | Height: | Size: 367 B |
@@ -1,3 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386 13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5 8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761 13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z" fill="#CCCAC2"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu"><line x1="4" x2="20" y1="12" y2="12"/><line x1="4" x2="20" y1="6" y2="6"/><line x1="4" x2="20" y1="18" y2="18"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 552 B After Width: | Height: | Size: 327 B |
@@ -1,4 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
|
||||
<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-minimize-2"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" x2="21" y1="10" y2="3"/><line x1="3" x2="10" y1="21" y2="14"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 297 B After Width: | Height: | Size: 371 B |
@@ -1,8 +1 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M9.42503 3.44136C10.0561 3.23654 10.7837 3.2402 11.3792 3.54623C12.7532 4.25224 13.3477 6.07191 12.7946 8C12.5465 8.8649 12.1102 9.70472 11.1861 10.5524C10.262 11.4 8.98034 11.9 8.38571 11.9C8.17269 11.9 8 11.7321 8 11.525C8 11.3179 8.17644 11.15 8.38571 11.15C9.06497 11.15 9.67189 10.7804 10.3906 10.236C10.9406 9.8193 11.3701 9.28633 11.608 8.82191C12.0628 7.93367 12.0782 6.68174 11.3433 6.34901C10.9904 6.73455 10.5295 6.95946 9.97725 6.95946C8.7773 6.95946 8.0701 5.99412 8.10051 5.12009C8.12957 4.28474 8.66032 3.68954 9.42503 3.44136ZM3.42503 3.44136C4.05614 3.23654 4.78366 3.2402 5.37923 3.54623C6.7532 4.25224 7.34766 6.07191 6.79462 8C6.54654 8.8649 6.11019 9.70472 5.1861 10.5524C4.26201 11.4 2.98034 11.9 2.38571 11.9C2.17269 11.9 2 11.7321 2 11.525C2 11.3179 2.17644 11.15 2.38571 11.15C3.06497 11.15 3.67189 10.7804 4.39058 10.236C4.94065 9.8193 5.37014 9.28633 5.60797 8.82191C6.06282 7.93367 6.07821 6.68174 5.3433 6.34901C4.99037 6.73455 4.52948 6.95946 3.97725 6.95946C2.7773 6.95946 2.0701 5.99412 2.10051 5.12009C2.12957 4.28474 2.66032 3.68954 3.42503 3.44136Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-quote"><path d="M17 6H3"/><path d="M21 12H8"/><path d="M21 18H8"/><path d="M3 12v6"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 299 B |
1
assets/icons/snip.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scissors"><circle cx="6" cy="6" r="3"/><path d="M8.12 8.12 12 12"/><path d="M20 4 8.12 15.88"/><circle cx="6" cy="18" r="3"/><path d="M14.8 14.8 20 20"/></svg>
|
||||
|
After Width: | Height: | Size: 362 B |
@@ -1 +0,0 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.81832 0.68179C7.64258 0.506054 7.35766 0.506054 7.18192 0.68179L5.18192 2.68179C5.00619 2.85753 5.00619 3.14245 5.18192 3.31819C5.35766 3.49392 5.64258 3.49392 5.81832 3.31819L7.05012 2.08638L7.05012 5.50023C7.05012 5.74876 7.25159 5.95023 7.50012 5.95023C7.74865 5.95023 7.95012 5.74876 7.95012 5.50023L7.95012 2.08638L9.18192 3.31819C9.35766 3.49392 9.64258 3.49392 9.81832 3.31819C9.99406 3.14245 9.99406 2.85753 9.81832 2.68179L7.81832 0.68179ZM7.95012 12.9136V9.50023C7.95012 9.2517 7.74865 9.05023 7.50012 9.05023C7.25159 9.05023 7.05012 9.2517 7.05012 9.50023V12.9136L5.81832 11.6818C5.64258 11.5061 5.35766 11.5061 5.18192 11.6818C5.00619 11.8575 5.00619 12.1424 5.18192 12.3182L7.18192 14.3182C7.26632 14.4026 7.38077 14.45 7.50012 14.45C7.61947 14.45 7.73393 14.4026 7.81832 14.3182L9.81832 12.3182C9.99406 12.1424 9.99406 11.8575 9.81832 11.6818C9.64258 11.5061 9.35766 11.5061 9.18192 11.6818L7.95012 12.9136ZM1.49994 7.00017C1.2238 7.00017 0.999939 7.22403 0.999939 7.50017C0.999939 7.77631 1.2238 8.00017 1.49994 8.00017L13.4999 8.00017C13.7761 8.00017 13.9999 7.77631 13.9999 7.50017C13.9999 7.22403 13.7761 7.00017 13.4999 7.00017L1.49994 7.00017Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -17,18 +17,9 @@
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k u": "pane::CloseCleanItems",
|
||||
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize",
|
||||
"cmd--": "zed::DecreaseBufferFontSize",
|
||||
@@ -38,15 +29,7 @@
|
||||
"cmd-h": "zed::Hide",
|
||||
"alt-cmd-h": "zed::HideOthers",
|
||||
"cmd-m": "zed::Minimize",
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open",
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"shift-escape": "workspace::ToggleZoom"
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -284,6 +267,15 @@
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k u": "pane::CloseCleanItems",
|
||||
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||
"cmd-f": "project_search::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
@@ -389,6 +381,14 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
"cmd-2": ["workspace::ActivatePane", 1],
|
||||
"cmd-3": ["workspace::ActivatePane", 2],
|
||||
@@ -530,12 +530,17 @@
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"space": "project_panel::Open",
|
||||
"backspace": "project_panel::Delete",
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
|
||||
@@ -143,7 +143,11 @@
|
||||
// Whether to show the git status in the project panel.
|
||||
"git_status": true,
|
||||
// Amount of indentation for nested items.
|
||||
"indent_size": 20
|
||||
"indent_size": 20,
|
||||
// Whether to reveal it in the project panel automatically,
|
||||
// when a corresponding project entry becomes active.
|
||||
// Gitignored entries are never auto revealed.
|
||||
"auto_reveal_entries": true
|
||||
},
|
||||
"collaboration_panel": {
|
||||
// Whether to show the collaboration panel button in the status bar.
|
||||
|
||||
@@ -17,5 +17,8 @@
|
||||
"file_name": "rose-pine-dawn.json",
|
||||
"appearance": "light"
|
||||
}
|
||||
]
|
||||
],
|
||||
"syntax": {
|
||||
"function": ["entity.name"]
|
||||
}
|
||||
}
|
||||
|
||||
28
crates/activity_indicator2/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "activity_indicator2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/activity_indicator.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
auto_update = { path = "../auto_update2", package = "auto_update2" }
|
||||
editor = { path = "../editor2", package = "editor2" }
|
||||
language = { path = "../language2", package = "language2" }
|
||||
gpui = { path = "../gpui2", package = "gpui2" }
|
||||
project = { path = "../project2", package = "project2" }
|
||||
settings = { path = "../settings2", package = "settings2" }
|
||||
ui = { path = "../ui2", package = "ui2" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme2", package = "theme2" }
|
||||
workspace = { path = "../workspace2", package = "workspace2" }
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
|
||||
333
crates/activity_indicator2/src/activity_indicator.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||
use editor::Editor;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model,
|
||||
ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View,
|
||||
ViewContext, VisualContext as _,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||
use ui::h_stack;
|
||||
use util::ResultExt;
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
actions!(activity_indicator, [ShowErrorMessage]);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
||||
const WARNING_ICON: &str = "icons/warning.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
}
|
||||
|
||||
pub struct ActivityIndicator {
|
||||
statuses: Vec<LspStatus>,
|
||||
project: Model<Project>,
|
||||
auto_updater: Option<Model<AutoUpdater>>,
|
||||
}
|
||||
|
||||
struct LspStatus {
|
||||
name: Arc<str>,
|
||||
status: LanguageServerBinaryStatus,
|
||||
}
|
||||
|
||||
struct PendingWork<'a> {
|
||||
language_server_name: &'a str,
|
||||
progress_token: &'a str,
|
||||
progress: &'a LanguageServerProgress,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Content {
|
||||
icon: Option<&'static str>,
|
||||
message: String,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
}
|
||||
|
||||
impl ActivityIndicator {
|
||||
pub fn new(
|
||||
workspace: &mut Workspace,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> View<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let this = cx.build_view(|cx: &mut ViewContext<Self>| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((language, event)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != language.name());
|
||||
this.statuses.push(LspStatus {
|
||||
name: language.name(),
|
||||
status: event,
|
||||
});
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
// cx.observe_active_labeled_tasks(|_, cx| cx.notify())
|
||||
// .detach();
|
||||
|
||||
Self {
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
auto_updater,
|
||||
}
|
||||
});
|
||||
|
||||
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
||||
Event::ShowError { lsp_name, error } => {
|
||||
if let Some(buffer) = project
|
||||
.update(cx, |project, cx| project.create_buffer(error, None, cx))
|
||||
.log_err()
|
||||
{
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(0..0, format!("Language server error: {}\n\n", lsp_name))],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.build_view(|cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), cx)
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
self.statuses.retain(|status| {
|
||||
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
error: error.clone(),
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.dismiss_error(cx);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn pending_language_server_work<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = PendingWork<'a>> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.language_server_statuses()
|
||||
.rev()
|
||||
.filter_map(|status| {
|
||||
if status.pending_work.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut pending_work = status
|
||||
.pending_work
|
||||
.iter()
|
||||
.map(|(token, progress)| PendingWork {
|
||||
language_server_name: status.name.as_str(),
|
||||
progress_token: token.as_str(),
|
||||
progress,
|
||||
})
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
|
||||
Some(pending_work)
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
language_server_name,
|
||||
progress_token,
|
||||
progress,
|
||||
}) = pending_work.next()
|
||||
{
|
||||
let mut message = language_server_name.to_string();
|
||||
|
||||
message.push_str(": ");
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(progress_message);
|
||||
} else {
|
||||
message.push_str(progress_token);
|
||||
}
|
||||
|
||||
if let Some(percentage) = progress.percentage {
|
||||
write!(&mut message, " ({}%)", percentage).unwrap();
|
||||
}
|
||||
|
||||
let additional_work_count = pending_work.count();
|
||||
if additional_work_count > 0 {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return Content {
|
||||
icon: None,
|
||||
message,
|
||||
on_click: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Show any language server installation info.
|
||||
let mut downloading = SmallVec::<[_; 3]>::new();
|
||||
let mut checking_for_update = SmallVec::<[_; 3]>::new();
|
||||
let mut failed = SmallVec::<[_; 3]>::new();
|
||||
for status in &self.statuses {
|
||||
let name = status.name.clone();
|
||||
match status.status {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
|
||||
LanguageServerBinaryStatus::Downloading => downloading.push(name),
|
||||
LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
|
||||
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Downloading {} language server{}...",
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
on_click: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Checking for updates to {} language server{}...",
|
||||
checking_for_update.join(", "),
|
||||
if checking_for_update.len() > 1 {
|
||||
"s"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
on_click: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: format!(
|
||||
"Failed to download {} language server{}. Click to show error.",
|
||||
failed.join(", "),
|
||||
if failed.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.show_error_message(&Default::default(), cx)
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
workspace::restart(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: "Auto update failed".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
};
|
||||
}
|
||||
|
||||
// todo!(show active tasks)
|
||||
// if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
||||
// return Content {
|
||||
// icon: None,
|
||||
// message: most_recent_active_task.to_string(),
|
||||
// on_click: None,
|
||||
// };
|
||||
// }
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActivityIndicator {}
|
||||
|
||||
impl Render for ActivityIndicator {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let content = self.content_to_render(cx);
|
||||
|
||||
let mut result = h_stack()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
.on_action(cx.listener(Self::dismiss_error_message));
|
||||
|
||||
if let Some(on_click) = content.on_click {
|
||||
result = result
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
on_click(this, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
result
|
||||
.children(content.icon.map(|icon| svg().path(icon)))
|
||||
.child(SharedString::from(content.message))
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ActivityIndicator {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ pub enum ProviderCredential {
|
||||
NotNeeded,
|
||||
}
|
||||
|
||||
pub trait CredentialProvider {
|
||||
pub trait CredentialProvider: Send + Sync {
|
||||
fn has_credentials(&self) -> bool;
|
||||
fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential;
|
||||
fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential);
|
||||
|
||||
@@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent {
|
||||
|
||||
pub async fn stream_completion(
|
||||
credential: ProviderCredential,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
request: Box<dyn CompletionRequest>,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
let api_key = match credential {
|
||||
@@ -197,11 +197,11 @@ pub async fn stream_completion(
|
||||
pub struct OpenAICompletionProvider {
|
||||
model: OpenAILanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl OpenAICompletionProvider {
|
||||
pub fn new(model_name: &str, executor: Arc<BackgroundExecutor>) -> Self {
|
||||
pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
|
||||
let model = OpenAILanguageModel::load(model_name);
|
||||
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
|
||||
Self {
|
||||
|
||||
@@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider {
|
||||
model: OpenAILanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
pub client: Arc<dyn HttpClient>,
|
||||
pub executor: Arc<BackgroundExecutor>,
|
||||
pub executor: BackgroundExecutor,
|
||||
rate_limit_count_rx: watch::Receiver<Option<Instant>>,
|
||||
rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
|
||||
}
|
||||
@@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage {
|
||||
}
|
||||
|
||||
impl OpenAIEmbeddingProvider {
|
||||
pub fn new(client: Arc<dyn HttpClient>, executor: Arc<BackgroundExecutor>) -> Self {
|
||||
pub fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
|
||||
let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
|
||||
let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
|
||||
|
||||
|
||||
@@ -1218,6 +1218,31 @@ impl View for AssistantPanel {
|
||||
let style = &theme.assistant;
|
||||
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Text::new(
|
||||
"To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" - Having a subscription for another service like GitHub Copilot won't work.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" - You can create a api key at: platform.openai.com/api-keys",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" ",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
"Paste your OpenAI API key and press Enter to use the assistant",
|
||||
@@ -1231,6 +1256,20 @@ impl View for AssistantPanel {
|
||||
.with_style(style.api_key_editor.container)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" ",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
"Click on the Z button in the status bar to close this panel.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.api_key_prompt.container)
|
||||
.aligned()
|
||||
|
||||
54
crates/assistant2/Cargo.toml
Normal file
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ai = { package = "ai2", path = "../ai2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
collections = { path = "../collections"}
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fs = { package = "fs2", path = "../fs2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
uuid.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ai = { package = "ai2", path = "../ai2", features = ["test-support"]}
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
63
crates/assistant2/README.zmd
Normal file
@@ -0,0 +1,63 @@
|
||||
> Hello, I'm Nathan Sobo.
|
||||
I'm currently your sole creator and the leader of Zed Industries.
|
||||
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
|
||||
|
||||
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
|
||||
|
||||
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
|
||||
|
||||
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
|
||||
|
||||
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
|
||||
|
||||
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
|
||||
|
||||
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
|
||||
|
||||
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
|
||||
|
||||
@nathan> #zed, can you describe the conversation UI as you currently understand it.
|
||||
|
||||
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
|
||||
|
||||
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
|
||||
|
||||
@nathan> What details around the conversation view are worth exploring?
|
||||
|
||||
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
|
||||
|
||||
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
|
||||
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
|
||||
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
|
||||
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
|
||||
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
|
||||
|
||||
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
|
||||
|
||||
@nathan> @nate, which of these would you like to explore?
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
#zed describe a hypothetical conversation UI that meets the description.
|
||||
|
||||
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
|
||||
|
||||
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
|
||||
|
||||
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
|
||||
|
||||
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
|
||||
|
||||
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
|
||||
|
||||
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
|
||||
|
||||
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
|
||||
|
||||
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
|
||||
|
||||
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.
|
||||
3
crates/assistant2/features.zmd
Normal file
@@ -0,0 +1,3 @@
|
||||
Push content to a deeper layer.
|
||||
A context can have multiple sublayers.
|
||||
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.
|
||||
129
crates/assistant2/src/assistant.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
pub mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod codegen;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
|
||||
use ai::providers::open_ai::Role;
|
||||
use anyhow::Result;
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
use assistant_settings::OpenAIModel;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{actions, AppContext, SharedString};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
actions!(
|
||||
assistant,
|
||||
[
|
||||
NewConversation,
|
||||
Assist,
|
||||
Split,
|
||||
CycleMessageRole,
|
||||
QuoteSelection,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
sent_at: DateTime<Local>,
|
||||
status: MessageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum MessageStatus {
|
||||
Pending,
|
||||
Done,
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedMessage {
|
||||
id: MessageId,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversation {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
model: OpenAIModel,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
const VERSION: &'static str = "0.1.0";
|
||||
}
|
||||
|
||||
struct SavedConversationMetadata {
|
||||
title: String,
|
||||
path: PathBuf,
|
||||
mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
3533
crates/assistant2/src/assistant_panel.rs
Normal file
80
crates/assistant2/src/assistant_settings.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum OpenAIModel {
|
||||
#[serde(rename = "gpt-3.5-turbo-0613")]
|
||||
ThreePointFiveTurbo,
|
||||
#[serde(rename = "gpt-4-0613")]
|
||||
Four,
|
||||
#[serde(rename = "gpt-4-1106-preview")]
|
||||
FourTurbo,
|
||||
}
|
||||
|
||||
impl OpenAIModel {
|
||||
pub fn full_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
|
||||
OpenAIModel::Four => "gpt-4-0613",
|
||||
OpenAIModel::FourTurbo => "gpt-4-1106-preview",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
OpenAIModel::Four => "gpt-4",
|
||||
OpenAIModel::FourTurbo => "gpt-4-turbo",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(&self) -> Self {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
|
||||
OpenAIModel::Four => OpenAIModel::FourTurbo,
|
||||
OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
pub default_open_ai_model: OpenAIModel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
pub default_open_ai_model: Option<OpenAIModel>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
688
crates/assistant2/src/codegen.rs
Normal file
@@ -0,0 +1,688 @@
|
||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, CompletionRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use multi_buffer;
|
||||
use std::{cmp, future, ops::Range, sync::Arc};
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: Model<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||
if self.transaction_id == Some(*transaction_id) {
|
||||
self.transaction_id = None;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(Event::Undone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Range<Anchor> {
|
||||
match &self.kind {
|
||||
CodegenKind::Transform { range } => range.clone(),
|
||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &CodegenKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.idle
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: Box<dyn CompletionRequest>, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
|
||||
|
||||
let response = self.provider.complete(prompt);
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta = line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(selection_start.column as usize);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
|
||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
this.last_equal_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = this.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.transaction_id = Some(transaction);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
diff.await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
this.idle = true;
|
||||
if let Err(error) = result {
|
||||
this.error = Some(error);
|
||||
}
|
||||
cx.emit(Event::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
self.error.take();
|
||||
self.idle = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction_id) = self.transaction_id {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_invalid_spans_from_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_markdown_codeblock = false;
|
||||
let mut includes_start_or_end_span = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
||||
includes_start_or_end_span = true;
|
||||
|
||||
buffer = buffer
|
||||
.strip_prefix("<|S|>")
|
||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
||||
.unwrap_or(&buffer)
|
||||
.to_string();
|
||||
} else if buffer.ends_with("|E|>") {
|
||||
includes_start_or_end_span = true;
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with("|")
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
return future::ready(None);
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer == "" || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text = buffer.to_string();
|
||||
if starts_with_markdown_codeblock {
|
||||
text = text
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| text.strip_suffix("\n```"))
|
||||
.or_else(|| text.strip_suffix("\n``"))
|
||||
.or_else(|| text.strip_suffix("\n`"))
|
||||
.or_else(|| text.strip_suffix('\n'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
if includes_start_or_end_span {
|
||||
text = text
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix(">"))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use ai::test::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl CompletionRequest for DummyCompletionRequest {
|
||||
fn data(&self) -> serde_json::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Transform { range },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
" }",
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("CHUNK: {:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_past_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_before_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("{:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks(
|
||||
"```html\n```js\nLorem ipsum dolor\n```\n```",
|
||||
2
|
||||
))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
389
crates/assistant2/src/prompts.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
use ai::models::LanguageModel;
|
||||
use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
|
||||
use ai::prompts::file_context::FileContext;
|
||||
use ai::prompts::generate::GenerateInlineContent;
|
||||
use ai::prompts::preamble::EngineerPreamble;
|
||||
use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext};
|
||||
use ai::providers::open_ai::OpenAILanguageModel;
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
collapse: Range<usize>,
|
||||
keep: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let selected_range = selected_range.to_offset(buffer);
|
||||
let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
|
||||
Some(&grammar.embedding_config.as_ref()?.query)
|
||||
});
|
||||
let configs = ts_matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|g| g.embedding_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = Vec::new();
|
||||
while let Some(mat) = ts_matches.peek() {
|
||||
let config = &configs[mat.grammar_index];
|
||||
if let Some(collapse) = mat.captures.iter().find_map(|cap| {
|
||||
if Some(cap.index) == config.collapse_capture_ix {
|
||||
Some(cap.node.byte_range())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let mut keep = Vec::new();
|
||||
for capture in mat.captures.iter() {
|
||||
if Some(capture.index) == config.keep_capture_ix {
|
||||
keep.push(capture.node.byte_range());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ts_matches.advance();
|
||||
matches.push(Match { collapse, keep });
|
||||
} else {
|
||||
ts_matches.advance();
|
||||
}
|
||||
}
|
||||
matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
|
||||
let mut matches = matches.into_iter().peekable();
|
||||
|
||||
let mut summary = String::new();
|
||||
let mut offset = 0;
|
||||
let mut flushed_selection = false;
|
||||
while let Some(mat) = matches.next() {
|
||||
// Keep extending the collapsed range if the next match surrounds
|
||||
// the current one.
|
||||
while let Some(next_mat) = matches.peek() {
|
||||
if mat.collapse.start <= next_mat.collapse.start
|
||||
&& mat.collapse.end >= next_mat.collapse.end
|
||||
{
|
||||
matches.next().unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if offset > mat.collapse.start {
|
||||
// Skip collapsed nodes that have already been summarized.
|
||||
offset = cmp::max(offset, mat.collapse.end);
|
||||
continue;
|
||||
}
|
||||
|
||||
if offset <= selected_range.start && selected_range.start <= mat.collapse.end {
|
||||
if !flushed_selection {
|
||||
// The collapsed node ends after the selection starts, so we'll flush the selection first.
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
flushed_selection = true;
|
||||
}
|
||||
|
||||
// If the selection intersects the collapsed node, we won't collapse it.
|
||||
if selected_range.end >= mat.collapse.start {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..mat.collapse.start));
|
||||
for keep in mat.keep {
|
||||
summary.extend(buffer.text_for_range(keep));
|
||||
}
|
||||
offset = mat.collapse.end;
|
||||
}
|
||||
|
||||
// Flush selection if we haven't already done so.
|
||||
if !flushed_selection && offset <= selected_range.start {
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..buffer.len()));
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
search_results: Vec<PromptCodeSnippet>,
|
||||
model: &str,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
// Using new Prompt Templates
|
||||
let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
|
||||
let lang_name = if let Some(language_name) = language_name {
|
||||
Some(language_name.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let args = PromptArguments {
|
||||
model: openai_model,
|
||||
language_name: lang_name.clone(),
|
||||
project_name,
|
||||
snippets: search_results.clone(),
|
||||
reserved_tokens: 1000,
|
||||
buffer: Some(buffer),
|
||||
selected_range: Some(range),
|
||||
user_prompt: Some(user_prompt.clone()),
|
||||
};
|
||||
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(RepositoryContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(FileContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Mandatory,
|
||||
Box::new(GenerateInlineContent {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
let (prompt, _) = chain.generate(true)?;
|
||||
|
||||
anyhow::Ok(prompt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, Context};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use settings::SettingsStore;
|
||||
|
||||
pub(crate) fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_embedding_query(
|
||||
r#"
|
||||
(
|
||||
[(line_comment) (attribute_item)]* @context
|
||||
.
|
||||
[
|
||||
(struct_item
|
||||
name: (_) @name)
|
||||
|
||||
(enum_item
|
||||
name: (_) @name)
|
||||
|
||||
(impl_item
|
||||
trait: (_)? @name
|
||||
"for"? @name
|
||||
type: (_) @name)
|
||||
|
||||
(trait_item
|
||||
name: (_) @name)
|
||||
|
||||
(function_item
|
||||
name: (_) @name
|
||||
body: (block
|
||||
"{" @keep
|
||||
"}" @keep) @collapse)
|
||||
|
||||
(macro_definition
|
||||
name: (_) @name)
|
||||
] @item
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_outline_for_prompt(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language_settings::init(cx);
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
self.a
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
<|S|>a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let <|S|a |E|>= 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
<|S|>
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
<|S|>"}
|
||||
);
|
||||
|
||||
// Ensure nested functions get collapsed properly.
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
let a = 30;
|
||||
fn nested() -> usize {
|
||||
3
|
||||
}
|
||||
self.a + nested()
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)),
|
||||
indoc! {"
|
||||
<|S|>struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
}
|
||||
293
crates/assistant2/src/streaming_diff.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use collections::HashMap;
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
struct Matrix {
|
||||
cells: Vec<f64>,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cells: Vec::new(),
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, rows: usize, cols: usize) {
|
||||
self.cells.resize(rows * cols, 0.);
|
||||
self.rows = rows;
|
||||
self.cols = cols;
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> f64 {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
self.cells[col * self.rows + row]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, value: f64) {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
|
||||
self.cells[col * self.rows + row] = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Matrix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f)?;
|
||||
for i in 0..self.rows {
|
||||
for j in 0..self.cols {
|
||||
write!(f, "{:5}", self.get(i, j))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Hunk {
|
||||
Insert { text: String },
|
||||
Remove { len: usize },
|
||||
Keep { len: usize },
|
||||
}
|
||||
|
||||
pub struct StreamingDiff {
|
||||
old: Vec<char>,
|
||||
new: Vec<char>,
|
||||
scores: Matrix,
|
||||
old_text_ix: usize,
|
||||
new_text_ix: usize,
|
||||
equal_runs: HashMap<(usize, usize), u32>,
|
||||
}
|
||||
|
||||
impl StreamingDiff {
|
||||
const INSERTION_SCORE: f64 = -1.;
|
||||
const DELETION_SCORE: f64 = -20.;
|
||||
const EQUALITY_BASE: f64 = 1.8;
|
||||
const MAX_EQUALITY_EXPONENT: i32 = 16;
|
||||
|
||||
pub fn new(old: String) -> Self {
|
||||
let old = old.chars().collect::<Vec<_>>();
|
||||
let mut scores = Matrix::new();
|
||||
scores.resize(old.len() + 1, 1);
|
||||
for i in 0..=old.len() {
|
||||
scores.set(i, 0, i as f64 * Self::DELETION_SCORE);
|
||||
}
|
||||
Self {
|
||||
old,
|
||||
new: Vec::new(),
|
||||
scores,
|
||||
old_text_ix: 0,
|
||||
new_text_ix: 0,
|
||||
equal_runs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
|
||||
self.new.extend(text.chars());
|
||||
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
|
||||
|
||||
for j in self.new_text_ix + 1..=self.new.len() {
|
||||
self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
|
||||
for i in 1..=self.old.len() {
|
||||
let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
|
||||
let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
|
||||
let equality_score = if self.old[i - 1] == self.new[j - 1] {
|
||||
let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
|
||||
equal_run += 1;
|
||||
self.equal_runs.insert((i, j), equal_run);
|
||||
|
||||
let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
|
||||
self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
};
|
||||
|
||||
let score = insertion_score.max(deletion_score).max(equality_score);
|
||||
self.scores.set(i, j, score);
|
||||
}
|
||||
}
|
||||
|
||||
let mut max_score = f64::NEG_INFINITY;
|
||||
let mut next_old_text_ix = self.old_text_ix;
|
||||
let next_new_text_ix = self.new.len();
|
||||
for i in self.old_text_ix..=self.old.len() {
|
||||
let score = self.scores.get(i, next_new_text_ix);
|
||||
if score > max_score {
|
||||
max_score = score;
|
||||
next_old_text_ix = i;
|
||||
}
|
||||
}
|
||||
|
||||
let hunks = self.backtrack(next_old_text_ix, next_new_text_ix);
|
||||
self.old_text_ix = next_old_text_ix;
|
||||
self.new_text_ix = next_new_text_ix;
|
||||
hunks
|
||||
}
|
||||
|
||||
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
|
||||
let mut pending_insert: Option<Range<usize>> = None;
|
||||
let mut hunks = Vec::new();
|
||||
let mut i = old_text_ix;
|
||||
let mut j = new_text_ix;
|
||||
while (i, j) != (self.old_text_ix, self.new_text_ix) {
|
||||
let insertion_score = if j > self.new_text_ix {
|
||||
Some((i, j - 1))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let deletion_score = if i > self.old_text_ix {
|
||||
Some((i - 1, j))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let equality_score = if i > self.old_text_ix && j > self.new_text_ix {
|
||||
if self.old[i - 1] == self.new[j - 1] {
|
||||
Some((i - 1, j - 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
|
||||
.iter()
|
||||
.max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if prev_i == i && prev_j == j - 1 {
|
||||
if let Some(pending_insert) = pending_insert.as_mut() {
|
||||
pending_insert.start = prev_j;
|
||||
} else {
|
||||
pending_insert = Some(prev_j..j);
|
||||
}
|
||||
} else {
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let char_len = self.old[i - 1].len_utf8();
|
||||
if prev_i == i - 1 && prev_j == j {
|
||||
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Remove { len: char_len })
|
||||
}
|
||||
} else {
|
||||
if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = prev_i;
|
||||
j = prev_j;
|
||||
}
|
||||
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
hunks.reverse();
|
||||
hunks
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Vec<Hunk> {
|
||||
self.backtrack(self.old.len(), self.new.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_diffs(mut rng: StdRng) {
|
||||
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
let new_text_len = env::var("NEW_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let old = util::RandomCharIter::new(&mut rng)
|
||||
.take(old_text_len)
|
||||
.collect::<String>();
|
||||
log::info!("old text: {:?}", old);
|
||||
|
||||
let mut diff = StreamingDiff::new(old.clone());
|
||||
let mut hunks = Vec::new();
|
||||
let mut new_len = 0;
|
||||
let mut new = String::new();
|
||||
while new_len < new_text_len {
|
||||
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
|
||||
let new_chunk = util::RandomCharIter::new(&mut rng)
|
||||
.take(new_len)
|
||||
.collect::<String>();
|
||||
log::info!("new chunk: {:?}", new_chunk);
|
||||
new_len += new_chunk_len;
|
||||
new.push_str(&new_chunk);
|
||||
let new_hunks = diff.push_new(&new_chunk);
|
||||
log::info!("hunks: {:?}", new_hunks);
|
||||
hunks.extend(new_hunks);
|
||||
}
|
||||
let final_hunks = diff.finish();
|
||||
log::info!("final hunks: {:?}", final_hunks);
|
||||
hunks.extend(final_hunks);
|
||||
|
||||
log::info!("new text: {:?}", new);
|
||||
let mut old_ix = 0;
|
||||
let mut new_ix = 0;
|
||||
let mut patched = String::new();
|
||||
for hunk in hunks {
|
||||
match hunk {
|
||||
Hunk::Keep { len } => {
|
||||
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
|
||||
patched.push_str(&old[old_ix..old_ix + len]);
|
||||
old_ix += len;
|
||||
new_ix += len;
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
old_ix += len;
|
||||
}
|
||||
Hunk::Insert { text } => {
|
||||
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
|
||||
patched.push_str(&text);
|
||||
new_ix += text.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(patched, new);
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,17 @@ use db::kvp::KEY_VALUE_STORE;
|
||||
use db::RELEASE_CHANNEL;
|
||||
use gpui::{
|
||||
actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
|
||||
ViewContext, VisualContext,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_derive::Serialize;
|
||||
use smol::io::AsyncReadExt;
|
||||
|
||||
use settings::{Settings, SettingsStore};
|
||||
use smol::{fs::File, process::Command};
|
||||
|
||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::channel::{AppCommitSha, ReleaseChannel};
|
||||
@@ -24,13 +26,7 @@ use workspace::Workspace;
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
//todo!(remove CheckThatAutoUpdaterWorks)
|
||||
actions!(
|
||||
Check,
|
||||
DismissErrorMessage,
|
||||
ViewReleaseNotes,
|
||||
CheckThatAutoUpdaterWorks
|
||||
);
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
@@ -85,24 +81,19 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace
|
||||
.register_action(|_, action: &Check, cx| check(action, cx))
|
||||
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
|
||||
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
workspace.register_action(|_, action: &Check, cx| check(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")
|
||||
// workspace.show_notification(0, _cx, |cx| {
|
||||
// cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
|
||||
// });
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Some(version) = *ZED_APP_VERSION {
|
||||
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
|
||||
let auto_updater = cx.build_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url);
|
||||
|
||||
@@ -124,15 +115,18 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
updater
|
||||
});
|
||||
cx.set_global(Some(auto_updater));
|
||||
//todo!(action)
|
||||
// cx.add_global_action(view_release_notes);
|
||||
// cx.add_action(UpdateNotification::dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut AppContext) {
|
||||
pub fn check(_: &Check, cx: &mut WindowContext) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Auto-updates disabled for non-bundled app.",
|
||||
&["Ok"],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use gpui::{
|
||||
div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
|
||||
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
|
||||
|
||||
@@ -18,6 +19,7 @@ impl Render for UpdateNotification {
|
||||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
v_stack()
|
||||
.on_action(cx.listener(UpdateNotification::dismiss))
|
||||
.elevation_3(cx)
|
||||
.p_4()
|
||||
.child(
|
||||
@@ -32,7 +34,7 @@ impl Render for UpdateNotification {
|
||||
.id("cancel")
|
||||
.child(IconElement::new(Icon::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -50,7 +52,7 @@ impl UpdateNotification {
|
||||
Self { version }
|
||||
}
|
||||
|
||||
pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent::Dismiss);
|
||||
pub fn dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
28
crates/breadcrumbs2/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "breadcrumbs2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/breadcrumbs.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
outline = { package = "outline2", path = "../outline2" }
|
||||
itertools = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
127
crates/breadcrumbs2/src/breadcrumbs.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
|
||||
ViewContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||
};
|
||||
|
||||
pub enum Event {
|
||||
UpdateLocation,
|
||||
}
|
||||
|
||||
pub struct Breadcrumbs {
|
||||
pane_focused: bool,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pane_focused: false,
|
||||
active_item: Default::default(),
|
||||
subscription: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Breadcrumbs {}
|
||||
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
|
||||
impl Render for Breadcrumbs {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let element = h_stack().text_ui();
|
||||
|
||||
let Some(active_item) = &self
|
||||
.active_item
|
||||
.as_ref()
|
||||
.filter(|item| item.downcast::<editor::Editor>().is_some())
|
||||
else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||
StyledText::new(segment.text)
|
||||
.with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default())
|
||||
.into_any()
|
||||
});
|
||||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||
Label::new("›").into_any_element()
|
||||
});
|
||||
|
||||
let editor = active_item
|
||||
.downcast::<Editor>()
|
||||
.map(|editor| editor.downgrade());
|
||||
|
||||
element.child(
|
||||
ButtonLike::new("toggle outline view")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(h_stack().gap_1().children(breadcrumbs))
|
||||
.on_click(move |_, cx| {
|
||||
if let Some(editor) = editor.as_ref().and_then(|editor| editor.upgrade()) {
|
||||
outline::toggle(editor, &outline::Toggle, cx)
|
||||
}
|
||||
})
|
||||
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for Breadcrumbs {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_item = None;
|
||||
if let Some(item) = active_pane_item {
|
||||
let this = cx.view().downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(Event::UpdateLocation);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
));
|
||||
self.active_item = Some(item.boxed_clone());
|
||||
item.breadcrumb_location(cx)
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
// fn location_for_event(
|
||||
// &self,
|
||||
// _: &Event,
|
||||
// current_location: ToolbarItemLocation,
|
||||
// cx: &AppContext,
|
||||
// ) -> ToolbarItemLocation {
|
||||
// if let Some(active_item) = self.active_item.as_ref() {
|
||||
// active_item.breadcrumb_location(cx)
|
||||
// } else {
|
||||
// current_location
|
||||
// }
|
||||
// }
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
self.pane_focused = pane_focused;
|
||||
}
|
||||
}
|
||||
@@ -31,9 +31,7 @@ media = { path = "../media" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
util = { path = "../util" }
|
||||
ui = {package = "ui2", path = "../ui2"}
|
||||
workspace = {package = "workspace2", path = "../workspace2"}
|
||||
async-trait.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
async-broadcast = "0.4"
|
||||
futures.workspace = true
|
||||
|
||||
@@ -1,32 +1,25 @@
|
||||
pub mod call_settings;
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
mod shared_screen;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use audio::Audio;
|
||||
use call_settings::CallSettings;
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE,
|
||||
};
|
||||
use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
|
||||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
|
||||
Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
|
||||
WeakModel,
|
||||
};
|
||||
pub use participant::ParticipantLocation;
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use room::Event;
|
||||
pub use room::Room;
|
||||
use settings::Settings;
|
||||
use shared_screen::SharedScreen;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
|
||||
|
||||
pub use participant::ParticipantLocation;
|
||||
pub use room::Room;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
|
||||
CallSettings::register(cx);
|
||||
@@ -334,55 +327,12 @@ impl ActiveCall {
|
||||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
requesting_window: Option<WindowHandle<Workspace>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return cx.spawn(|_, _| async move {
|
||||
todo!();
|
||||
// let future = room.update(&mut cx, |room, cx| {
|
||||
// room.most_active_project(cx).map(|(host, project)| {
|
||||
// room.join_project(project, host, app_state.clone(), cx)
|
||||
// })
|
||||
// })
|
||||
|
||||
// if let Some(future) = future {
|
||||
// future.await?;
|
||||
// }
|
||||
|
||||
// Ok(Some(room))
|
||||
});
|
||||
}
|
||||
|
||||
let should_prompt = room.update(cx, |room, _| {
|
||||
room.channel_id().is_some()
|
||||
&& room.is_sharing_project()
|
||||
&& room.remote_participants().len() > 0
|
||||
});
|
||||
if should_prompt && requesting_window.is_some() {
|
||||
return cx.spawn(|this, mut cx| async move {
|
||||
let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
|
||||
&["Yes, Join Channel", "Cancel"],
|
||||
)
|
||||
})?;
|
||||
if answer.await? == 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
room.update(&mut cx, |room, cx| room.clear_state(cx))?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.join_channel(channel_id, requesting_window, cx)
|
||||
})?
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
if room.read(cx).channel_id().is_some() {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
}
|
||||
}
|
||||
@@ -555,208 +505,6 @@ pub fn report_call_event_for_channel(
|
||||
)
|
||||
}
|
||||
|
||||
pub struct Call {
|
||||
active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
|
||||
parent_workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl Call {
|
||||
pub fn new(
|
||||
parent_workspace: WeakView<Workspace>,
|
||||
cx: &mut ViewContext<'_, Workspace>,
|
||||
) -> Box<dyn CallHandler> {
|
||||
let mut active_call = None;
|
||||
if cx.has_global::<Model<ActiveCall>>() {
|
||||
let call = cx.global::<Model<ActiveCall>>().clone();
|
||||
let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)];
|
||||
active_call = Some((call, subscriptions));
|
||||
}
|
||||
Box::new(Self {
|
||||
active_call,
|
||||
parent_workspace,
|
||||
})
|
||||
}
|
||||
fn on_active_call_event(
|
||||
workspace: &mut Workspace,
|
||||
_: Model<ActiveCall>,
|
||||
event: &room::Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
room::Event::ParticipantLocationChanged { participant_id }
|
||||
| room::Event::RemoteVideoTracksChanged { participant_id } => {
|
||||
workspace.leader_updated(*participant_id, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl CallHandler for Call {
|
||||
fn peer_state(
|
||||
&mut self,
|
||||
leader_id: PeerId,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<(bool, bool)> {
|
||||
let (call, _) = self.active_call.as_ref()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
||||
|
||||
let leader_in_this_app;
|
||||
let leader_in_this_project;
|
||||
match participant.location {
|
||||
ParticipantLocation::SharedProject { project_id } => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = Some(project_id)
|
||||
== self
|
||||
.parent_workspace
|
||||
.update(cx, |this, cx| this.project().read(cx).remote_id())
|
||||
.log_err()
|
||||
.flatten();
|
||||
}
|
||||
ParticipantLocation::UnsharedProject => {
|
||||
leader_in_this_app = true;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
ParticipantLocation::External => {
|
||||
leader_in_this_app = false;
|
||||
leader_in_this_project = false;
|
||||
}
|
||||
};
|
||||
|
||||
Some((leader_in_this_project, leader_in_this_app))
|
||||
}
|
||||
|
||||
fn shared_screen_for_peer(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
pane: &View<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
let (call, _) = self.active_call.as_ref()?;
|
||||
let room = call.read(cx).room()?.read(cx);
|
||||
let participant = room.remote_participant_for_peer_id(peer_id)?;
|
||||
let track = participant.video_tracks.values().next()?.clone();
|
||||
let user = participant.user.clone();
|
||||
for item in pane.read(cx).items_of_type::<SharedScreen>() {
|
||||
if item.read(cx).peer_id == peer_id {
|
||||
return Some(Box::new(item));
|
||||
}
|
||||
}
|
||||
|
||||
Some(Box::new(cx.build_view(|cx| {
|
||||
SharedScreen::new(&track, peer_id, user.clone(), cx)
|
||||
})))
|
||||
}
|
||||
fn room_id(&self, cx: &AppContext) -> Option<u64> {
|
||||
Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
|
||||
}
|
||||
fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
|
||||
let Some((call, _)) = self.active_call.as_ref() else {
|
||||
return Task::ready(Err(anyhow!("Cannot exit a call; not in a call")));
|
||||
};
|
||||
|
||||
call.update(cx, |this, cx| this.hang_up(cx))
|
||||
}
|
||||
fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
|
||||
ActiveCall::global(cx).read(cx).location().cloned()
|
||||
}
|
||||
fn invite(
|
||||
&mut self,
|
||||
called_user_id: u64,
|
||||
initial_project: Option<Model<Project>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<()>> {
|
||||
ActiveCall::global(cx).update(cx, |this, cx| {
|
||||
this.invite(called_user_id, initial_project, cx)
|
||||
})
|
||||
}
|
||||
fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0.read(cx).room().map(|room| {
|
||||
room.read(cx)
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.map(|participant| {
|
||||
(participant.1.user.clone(), participant.1.peer_id.clone())
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
fn is_muted(&self, cx: &AppContext) -> Option<bool> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).is_muted(cx))
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
fn toggle_mute(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
let room = room.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
|
||||
.await
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn toggle_screen_share(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
room.update(cx, |this, cx| {
|
||||
if this.is_screen_sharing() {
|
||||
this.unshare_screen(cx).log_err();
|
||||
} else {
|
||||
let t = this.share_screen(cx);
|
||||
cx.spawn(move |_, _| async move {
|
||||
t.await.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn toggle_deafen(&self, cx: &mut AppContext) {
|
||||
self.active_call.as_ref().map(|call| {
|
||||
call.0.update(cx, |this, cx| {
|
||||
this.room().map(|room| {
|
||||
room.update(cx, |this, cx| {
|
||||
this.toggle_deafen(cx).log_err();
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
|
||||
self.active_call
|
||||
.as_ref()
|
||||
.map(|call| {
|
||||
call.0
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).is_deafened())
|
||||
})
|
||||
.flatten()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use gpui::TestAppContext;
|
||||
|
||||
@@ -4,7 +4,7 @@ use client::{proto, User};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakModel;
|
||||
pub use live_kit_client::Frame;
|
||||
pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
use crate::participant::{Frame, RemoteVideoTrack};
|
||||
use anyhow::Result;
|
||||
use client::{proto::PeerId, User};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
div, AppContext, Div, Element, EventEmitter, FocusHandle, FocusableView, ParentElement, Render,
|
||||
SharedString, Task, View, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use workspace::{item::Item, ItemNavHistory, WorkspaceId};
|
||||
|
||||
pub enum Event {
|
||||
Close,
|
||||
}
|
||||
|
||||
pub struct SharedScreen {
|
||||
track: Weak<RemoteVideoTrack>,
|
||||
frame: Option<Frame>,
|
||||
// temporary addition just to render something interactive.
|
||||
current_frame_id: usize,
|
||||
pub peer_id: PeerId,
|
||||
user: Arc<User>,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
_maintain_frame: Task<Result<()>>,
|
||||
focus: FocusHandle,
|
||||
}
|
||||
|
||||
impl SharedScreen {
|
||||
pub fn new(
|
||||
track: &Arc<RemoteVideoTrack>,
|
||||
peer_id: PeerId,
|
||||
user: Arc<User>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.focus_handle();
|
||||
let mut frames = track.frames();
|
||||
Self {
|
||||
track: Arc::downgrade(track),
|
||||
frame: None,
|
||||
peer_id,
|
||||
user,
|
||||
nav_history: Default::default(),
|
||||
_maintain_frame: cx.spawn(|this, mut cx| async move {
|
||||
while let Some(frame) = frames.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.frame = Some(frame);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
this.update(&mut cx, |_, cx| cx.emit(Event::Close))?;
|
||||
Ok(())
|
||||
}),
|
||||
focus: cx.focus_handle(),
|
||||
current_frame_id: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for SharedScreen {}
|
||||
impl EventEmitter<workspace::item::ItemEvent> for SharedScreen {}
|
||||
|
||||
impl FocusableView for SharedScreen {
|
||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||
self.focus.clone()
|
||||
}
|
||||
}
|
||||
impl Render for SharedScreen {
|
||||
type Element = Div;
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
|
||||
let frame = self.frame.clone();
|
||||
let frame_id = self.current_frame_id;
|
||||
self.current_frame_id = self.current_frame_id.wrapping_add(1);
|
||||
div().children(frame.map(|_| {
|
||||
ui::Label::new(frame_id.to_string()).color(ui::Color::Error)
|
||||
// img().data(Arc::new(ImageData::new(image::ImageBuffer::new(
|
||||
// frame.width() as u32,
|
||||
// frame.height() as u32,
|
||||
// ))))
|
||||
}))
|
||||
}
|
||||
}
|
||||
// impl View for SharedScreen {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "SharedScreen"
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// enum Focus {}
|
||||
|
||||
// let frame = self.frame.clone();
|
||||
// MouseEventHandler::new::<Focus, _>(0, cx, |_, cx| {
|
||||
// Canvas::new(move |bounds, _, _, cx| {
|
||||
// if let Some(frame) = frame.clone() {
|
||||
// let size = constrain_size_preserving_aspect_ratio(
|
||||
// bounds.size(),
|
||||
// vec2f(frame.width() as f32, frame.height() as f32),
|
||||
// );
|
||||
// let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.;
|
||||
// cx.scene().push_surface(gpui::platform::mac::Surface {
|
||||
// bounds: RectF::new(origin, size),
|
||||
// image_buffer: frame.image(),
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(theme::current(cx).shared_screen)
|
||||
// })
|
||||
// .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent())
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Item for SharedScreen {
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
|
||||
Some(format!("{}'s screen", self.user.github_login).into())
|
||||
}
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(nav_history) = self.nav_history.as_mut() {
|
||||
nav_history.push::<()>(None, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(&self, _: Option<usize>, _: &WindowContext<'_>) -> gpui::AnyElement {
|
||||
div().child("Shared screen").into_any()
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Svg::new("icons/desktop.svg")
|
||||
// .with_color(style.label.text.color)
|
||||
// .constrained()
|
||||
// .with_width(style.type_icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_margin_right(style.spacing),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// format!("{}'s screen", self.user.github_login),
|
||||
// style.label.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// )
|
||||
// .into_any()
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||
self.nav_history = Some(history);
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Self>> {
|
||||
let track = self.track.upgrade()?;
|
||||
Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@ use collections::{hash_map, HashMap, HashSet};
|
||||
use db::RELEASE_CHANNEL;
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use rpc::{
|
||||
proto::{self, ChannelVisibility},
|
||||
@@ -46,7 +47,7 @@ pub struct ChannelStore {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub name: SharedString,
|
||||
pub visibility: proto::ChannelVisibility,
|
||||
pub role: proto::ChannelRole,
|
||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||
@@ -895,14 +896,16 @@ impl ChannelStore {
|
||||
.channel_invitations
|
||||
.binary_search_by_key(&channel.id, |c| c.id)
|
||||
{
|
||||
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
|
||||
Ok(ix) => {
|
||||
Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
|
||||
}
|
||||
Err(ix) => self.channel_invitations.insert(
|
||||
ix,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility(),
|
||||
role: channel.role(),
|
||||
name: channel.name,
|
||||
name: channel.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel.parent_path,
|
||||
|
||||
@@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
existing_channel.role = channel_proto.role();
|
||||
existing_channel.name = channel_proto.name;
|
||||
existing_channel.name = channel_proto.name.into();
|
||||
} else {
|
||||
self.channels_by_id.insert(
|
||||
channel_proto.id,
|
||||
@@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
id: channel_proto.id,
|
||||
visibility: channel_proto.visibility(),
|
||||
role: channel_proto.role(),
|
||||
name: channel_proto.name,
|
||||
name: channel_proto.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel_proto.parent_path,
|
||||
@@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
|
||||
let (parent_path, name) = channels_by_id
|
||||
.get(&id)
|
||||
.map_or((&[] as &[_], None), |channel| {
|
||||
(channel.parent_path.as_slice(), Some(channel.name.as_str()))
|
||||
(channel.parent_path.as_slice(), Some(channel.name.as_ref()))
|
||||
});
|
||||
parent_path
|
||||
.iter()
|
||||
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
||||
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
|
||||
.chain(name)
|
||||
}
|
||||
|
||||
|
||||
@@ -345,7 +345,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
fn init_test(cx: &mut AppContext) -> Model<ChannelStore> {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
|
||||
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
|
||||
@@ -346,7 +346,7 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TelemetrySettings {
|
||||
pub diagnostics: bool,
|
||||
pub metrics: bool,
|
||||
|
||||
@@ -11,8 +11,8 @@ use async_tungstenite::tungstenite::{
|
||||
http::{Request, StatusCode},
|
||||
};
|
||||
use futures::{
|
||||
future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _,
|
||||
TryStreamExt,
|
||||
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
|
||||
TryFutureExt as _, TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model,
|
||||
@@ -70,7 +70,7 @@ pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
actions!(SignIn, SignOut, Reconnect);
|
||||
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
TelemetrySettings::register(cx);
|
||||
@@ -1020,91 +1020,116 @@ impl Client {
|
||||
) -> Task<Result<Credentials>> {
|
||||
let http = self.http.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
// any other app running on the user's device.
|
||||
let (public_key, private_key) =
|
||||
rpc::auth::keypair().expect("failed to generate keypair for auth");
|
||||
let public_key_string =
|
||||
String::try_from(public_key).expect("failed to serialize public key for auth");
|
||||
let background = cx.background_executor().clone();
|
||||
|
||||
if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
|
||||
}
|
||||
let (open_url_tx, open_url_rx) = oneshot::channel::<String>();
|
||||
cx.update(|cx| {
|
||||
cx.spawn(move |cx| async move {
|
||||
let url = open_url_rx.await?;
|
||||
cx.update(|cx| cx.open_url(&url))
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
||||
let port = server.server_addr().port();
|
||||
let credentials = background
|
||||
.clone()
|
||||
.spawn(async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
// any other app running on the user's device.
|
||||
let (public_key, private_key) =
|
||||
rpc::auth::keypair().expect("failed to generate keypair for auth");
|
||||
let public_key_string = String::try_from(public_key)
|
||||
.expect("failed to serialize public key for auth");
|
||||
|
||||
// Open the Zed sign-in page in the user's browser, with query parameters that indicate
|
||||
// that the user is signing in from a Zed app running on the same device.
|
||||
let mut url = format!(
|
||||
"{}/native_app_signin?native_app_port={}&native_app_public_key={}",
|
||||
*ZED_SERVER_URL, port, public_key_string
|
||||
);
|
||||
if let Some((login, token)) =
|
||||
IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
|
||||
{
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
|
||||
log::info!("impersonating user @{}", impersonate_login);
|
||||
write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
|
||||
}
|
||||
// Start an HTTP server to receive the redirect from Zed's sign-in page.
|
||||
let server =
|
||||
tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
|
||||
let port = server.server_addr().port();
|
||||
|
||||
cx.update(|cx| cx.open_url(&url))?;
|
||||
// Open the Zed sign-in page in the user's browser, with query parameters that indicate
|
||||
// that the user is signing in from a Zed app running on the same device.
|
||||
let mut url = format!(
|
||||
"{}/native_app_signin?native_app_port={}&native_app_public_key={}",
|
||||
*ZED_SERVER_URL, port, public_key_string
|
||||
);
|
||||
|
||||
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
|
||||
// access token from the query params.
|
||||
//
|
||||
// TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
|
||||
// custom URL scheme instead of this local HTTP server.
|
||||
let (user_id, access_token) = cx
|
||||
.spawn(|_| async move {
|
||||
for _ in 0..100 {
|
||||
if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
|
||||
let path = req.url();
|
||||
let mut user_id = None;
|
||||
let mut access_token = None;
|
||||
let url = Url::parse(&format!("http://example.com{}", path))
|
||||
.context("failed to parse login notification url")?;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "access_token" {
|
||||
access_token = Some(value.to_string());
|
||||
} else if key == "user_id" {
|
||||
user_id = Some(value.to_string());
|
||||
if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
|
||||
log::info!("impersonating user @{}", impersonate_login);
|
||||
write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
|
||||
}
|
||||
|
||||
open_url_tx.send(url).log_err();
|
||||
|
||||
// Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
|
||||
// access token from the query params.
|
||||
//
|
||||
// TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
|
||||
// custom URL scheme instead of this local HTTP server.
|
||||
let (user_id, access_token) = background
|
||||
.spawn(async move {
|
||||
for _ in 0..100 {
|
||||
if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
|
||||
let path = req.url();
|
||||
let mut user_id = None;
|
||||
let mut access_token = None;
|
||||
let url = Url::parse(&format!("http://example.com{}", path))
|
||||
.context("failed to parse login notification url")?;
|
||||
for (key, value) in url.query_pairs() {
|
||||
if key == "access_token" {
|
||||
access_token = Some(value.to_string());
|
||||
} else if key == "user_id" {
|
||||
user_id = Some(value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let post_auth_url =
|
||||
format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
|
||||
req.respond(
|
||||
tiny_http::Response::empty(302).with_header(
|
||||
tiny_http::Header::from_bytes(
|
||||
&b"Location"[..],
|
||||
post_auth_url.as_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.context("failed to respond to login http request")?;
|
||||
return Ok((
|
||||
user_id
|
||||
.ok_or_else(|| anyhow!("missing user_id parameter"))?,
|
||||
access_token.ok_or_else(|| {
|
||||
anyhow!("missing access_token parameter")
|
||||
})?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let post_auth_url =
|
||||
format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
|
||||
req.respond(
|
||||
tiny_http::Response::empty(302).with_header(
|
||||
tiny_http::Header::from_bytes(
|
||||
&b"Location"[..],
|
||||
post_auth_url.as_bytes(),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
)
|
||||
.context("failed to respond to login http request")?;
|
||||
return Ok((
|
||||
user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?,
|
||||
access_token
|
||||
.ok_or_else(|| anyhow!("missing access_token parameter"))?,
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(anyhow!("didn't receive login redirect"))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Err(anyhow!("didn't receive login redirect"))
|
||||
let access_token = private_key
|
||||
.decrypt_string(&access_token)
|
||||
.context("failed to decrypt access token")?;
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
|
||||
let access_token = private_key
|
||||
.decrypt_string(&access_token)
|
||||
.context("failed to decrypt access token")?;
|
||||
cx.update(|cx| cx.activate(true))?;
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
access_token,
|
||||
})
|
||||
Ok(credentials)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use rpc::{
|
||||
ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use util::http::FakeHttpClient;
|
||||
|
||||
pub struct FakeServer {
|
||||
peer: Arc<Peer>,
|
||||
@@ -195,8 +194,7 @@ impl FakeServer {
|
||||
client: Arc<Client>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Model<UserStore> {
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client, http_client, cx));
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client, cx));
|
||||
assert_eq!(
|
||||
self.receive::<proto::GetUsers>()
|
||||
.await
|
||||
|
||||
@@ -2,13 +2,12 @@ use super::{proto, Client, Status, TypedEnvelope};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
||||
use gpui::{AsyncAppContext, EventEmitter, ImageData, Model, ModelContext, Task};
|
||||
use futures::{channel::mpsc, Future, StreamExt};
|
||||
use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task};
|
||||
use postage::{sink::Sink, watch};
|
||||
use rpc::proto::{RequestMessage, UsersResponse};
|
||||
use std::sync::{Arc, Weak};
|
||||
use text::ReplicaId;
|
||||
use util::http::HttpClient;
|
||||
use util::TryFutureExt as _;
|
||||
|
||||
pub type UserId = u64;
|
||||
@@ -20,7 +19,7 @@ pub struct ParticipantIndex(pub u32);
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub github_login: String,
|
||||
pub avatar: Option<Arc<ImageData>>,
|
||||
pub avatar_uri: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -76,7 +75,6 @@ pub struct UserStore {
|
||||
pending_contact_requests: HashMap<u64, usize>,
|
||||
invite_info: Option<InviteInfo>,
|
||||
client: Weak<Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
_maintain_contacts: Task<()>,
|
||||
_maintain_current_user: Task<Result<()>>,
|
||||
}
|
||||
@@ -112,11 +110,7 @@ enum UpdateContacts {
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
|
||||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
let (update_contacts_tx, mut update_contacts_rx) = mpsc::unbounded();
|
||||
let rpc_subscriptions = vec![
|
||||
@@ -134,7 +128,6 @@ impl UserStore {
|
||||
invite_info: None,
|
||||
client: Arc::downgrade(&client),
|
||||
update_contacts_tx,
|
||||
http,
|
||||
_maintain_contacts: cx.spawn(|this, mut cx| async move {
|
||||
let _subscriptions = rpc_subscriptions;
|
||||
while let Some(message) = update_contacts_rx.next().await {
|
||||
@@ -445,6 +438,12 @@ impl UserStore {
|
||||
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
|
||||
}
|
||||
|
||||
pub fn has_incoming_contact_request(&self, user_id: u64) -> bool {
|
||||
self.incoming_contact_requests
|
||||
.iter()
|
||||
.any(|user| user.id == user_id)
|
||||
}
|
||||
|
||||
pub fn respond_to_contact_request(
|
||||
&mut self,
|
||||
requester_id: u64,
|
||||
@@ -616,17 +615,14 @@ impl UserStore {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
let client = self.client.clone();
|
||||
let http = self.http.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(rpc) = client.upgrade() {
|
||||
let response = rpc.request(request).await.context("error loading users")?;
|
||||
let users = future::join_all(
|
||||
response
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|user| User::new(user, http.as_ref())),
|
||||
)
|
||||
.await;
|
||||
let users = response
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|user| User::new(user))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
for user in &users {
|
||||
@@ -659,11 +655,11 @@ impl UserStore {
|
||||
}
|
||||
|
||||
impl User {
|
||||
async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
|
||||
fn new(message: proto::User) -> Arc<Self> {
|
||||
Arc::new(User {
|
||||
id: message.id,
|
||||
github_login: message.github_login,
|
||||
avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
|
||||
avatar_uri: message.avatar_url.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -696,25 +692,3 @@ impl Collaborator {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// todo!("we probably don't need this now that we fetch")
|
||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||
let mut response = http
|
||||
.get(url, Default::default(), true)
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow!("avatar request failed {:?}", response.status()));
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
|
||||
let format = image::guess_format(&body)?;
|
||||
let image = image::load_from_memory_with_format(&body, format)?.into_bgra8();
|
||||
Ok(Arc::new(ImageData::new(image)))
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.29.0"
|
||||
version = "0.31.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -116,12 +116,14 @@ struct CreateUserResponse {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Panic {
|
||||
version: String,
|
||||
release_channel: String,
|
||||
backtrace_hash: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(panic))]
|
||||
async fn trace_panic(panic: Json<Panic>) -> Result<()> {
|
||||
tracing::error!(version = %panic.version, text = %panic.text, "panic report");
|
||||
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, backtrace_hash = %panic.backtrace_hash, text = %panic.text, "panic report");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1220,6 +1220,13 @@ impl Database {
|
||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||
}
|
||||
|
||||
new_parent_path = new_parent.path();
|
||||
new_parent_channel = Some(new_parent);
|
||||
} else {
|
||||
|
||||
@@ -450,6 +450,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Can't move a channel into its ancestor
|
||||
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
|
||||
@@ -4,8 +4,10 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use workspace::{
|
||||
dock::{test::TestPanel, DockPosition},
|
||||
@@ -1602,6 +1604,141 @@ async fn test_following_across_workspaces(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_into_excluded_file(
|
||||
deterministic: Arc<Deterministic>,
|
||||
mut cx_a: &mut TestAppContext,
|
||||
mut cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
for cx in [&mut cx_a, &mut cx_b] {
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git": {
|
||||
"COMMIT_EDITMSG": "write your commit message here",
|
||||
},
|
||||
"1.txt": "one\none\none",
|
||||
"2.txt": "two\ntwo\ntwo",
|
||||
"3.txt": "three\nthree\nthree",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
let workspace_a = window_a.root(cx_a);
|
||||
let peer_id_a = client_a.peer_id().unwrap();
|
||||
let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
let workspace_b = window_b.root(cx_b);
|
||||
|
||||
// Client A opens editors for a regular file and an excluded file.
|
||||
let editor_for_regular = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let editor_for_excluded_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
// Client A updates their selections in those editors
|
||||
editor_for_regular.update(cx_a, |editor, cx| {
|
||||
editor.handle_input("a", cx);
|
||||
editor.handle_input("b", cx);
|
||||
editor.handle_input("c", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
});
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
editor.select_all(&Default::default(), cx);
|
||||
editor.handle_input("new commit message", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![18..17]);
|
||||
});
|
||||
|
||||
// When client B starts following client A, currently visible file is replicated
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(peer_id_a, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
|
||||
Some((worktree_id, ".git/COMMIT_EDITMSG").into())
|
||||
);
|
||||
assert_eq!(
|
||||
editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![18..17]
|
||||
);
|
||||
|
||||
// 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 <b@b.b>", cx);
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"new commit messag\nCo-Authored-By: B <b@b.b>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
|
||||
@@ -2981,11 +2981,10 @@ async fn test_fs_operations(
|
||||
|
||||
let entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
assert_eq!(
|
||||
@@ -3010,7 +3009,6 @@ async fn test_fs_operations(
|
||||
.update(cx_b, |project, cx| {
|
||||
project.rename_entry(entry.id, Path::new("d.txt"), cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3034,11 +3032,10 @@ async fn test_fs_operations(
|
||||
|
||||
let dir_entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
assert_eq!(
|
||||
@@ -3061,25 +3058,19 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3120,9 +3111,7 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
.unwrap()
|
||||
project.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3941,7 +3930,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
// Ensure client B observes the new diagnostics.
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
&[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -3961,14 +3950,14 @@ async fn test_collaborating_with_diagnostics(
|
||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
let project_c_diagnostic_summaries =
|
||||
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>()
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
|
||||
})));
|
||||
project_c.update(cx_c, |_, cx| {
|
||||
let summaries = project_c_diagnostic_summaries.clone();
|
||||
cx.subscribe(&project_c, {
|
||||
move |p, _, event, cx| {
|
||||
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||
*summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
|
||||
*summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -4018,7 +4007,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
deterministic.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -4034,7 +4023,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
});
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -4097,13 +4086,22 @@ async fn test_collaborating_with_diagnostics(
|
||||
);
|
||||
deterministic.run_until_parked();
|
||||
project_a.read_with(cx_a, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
project
|
||||
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
|
||||
.unwrap()
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
|
||||
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||
collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] }
|
||||
|
||||
async-trait.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -116,12 +116,14 @@ struct CreateUserResponse {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Panic {
|
||||
version: String,
|
||||
release_channel: String,
|
||||
backtrace_hash: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(panic))]
|
||||
async fn trace_panic(panic: Json<Panic>) -> Result<()> {
|
||||
tracing::error!(version = %panic.version, text = %panic.text, "panic report");
|
||||
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, backtrace_hash = %panic.backtrace_hash, text = %panic.text, "panic report");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1220,6 +1220,13 @@ impl Database {
|
||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||
}
|
||||
|
||||
new_parent_path = new_parent.path();
|
||||
new_parent_channel = Some(new_parent);
|
||||
} else {
|
||||
|
||||
@@ -420,8 +420,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Dag is: zed - projects - livestreaming
|
||||
|
||||
// Move to same parent should be a no-op
|
||||
assert!(db
|
||||
.move_channel(projects_id, Some(zed_id), user_id)
|
||||
@@ -450,6 +448,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Can't move a channel into its ancestor
|
||||
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
|
||||
@@ -7,7 +7,7 @@ use call::ActiveCall;
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::User;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
@@ -46,13 +46,13 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
@@ -92,7 +92,7 @@ async fn test_core_channels(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
@@ -140,13 +140,13 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
@@ -168,19 +168,19 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".to_string(),
|
||||
name: "channel-c".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 2,
|
||||
},
|
||||
@@ -211,19 +211,19 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".to_string(),
|
||||
name: "channel-c".into(),
|
||||
depth: 2,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
@@ -245,7 +245,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -255,7 +255,7 @@ async fn test_core_channels(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -278,7 +278,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -309,7 +309,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a-renamed".to_string(),
|
||||
name: "channel-a-renamed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -364,8 +364,7 @@ async fn test_joining_channel_ancestor_member(
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |active_call, cx| active_call
|
||||
.join_channel(sub_id, None, cx))
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
|
||||
.await
|
||||
.is_ok());
|
||||
}
|
||||
@@ -395,9 +394,7 @@ async fn test_channel_room(
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -421,7 +418,7 @@ async fn test_channel_room(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: zed_id,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
@@ -445,9 +442,7 @@ async fn test_channel_room(
|
||||
});
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -564,16 +559,12 @@ async fn test_channel_room(
|
||||
});
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -617,9 +608,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(zed_id, None, cx)
|
||||
})
|
||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -638,7 +627,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(rust_id, None, cx)
|
||||
active_call.join_channel(rust_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -691,7 +680,7 @@ async fn test_permissions_update_while_invited(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -719,7 +708,7 @@ async fn test_permissions_update_while_invited(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -758,7 +747,7 @@ async fn test_channel_rename(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".to_string(),
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
@@ -770,7 +759,7 @@ async fn test_channel_rename(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".to_string(),
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -804,7 +793,7 @@ async fn test_call_from_channel(
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -899,7 +888,7 @@ async fn test_lost_channel_creation(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -923,13 +912,13 @@ async fn test_lost_channel_creation(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".to_string(),
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
@@ -954,13 +943,13 @@ async fn test_lost_channel_creation(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".to_string(),
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
@@ -1232,13 +1221,13 @@ async fn test_channel_membership_notifications(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".to_string(),
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
@@ -1261,13 +1250,13 @@ async fn test_channel_membership_notifications(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".to_string(),
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
],
|
||||
@@ -1297,7 +1286,7 @@ async fn test_guest_access(
|
||||
|
||||
// Non-members should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
@@ -1319,7 +1308,7 @@ async fn test_guest_access(
|
||||
|
||||
// Client B joins channel A as a guest
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1352,7 +1341,7 @@ async fn test_guest_access(
|
||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1383,7 +1372,7 @@ async fn test_invite_access(
|
||||
|
||||
// should not be allowed to join
|
||||
assert!(active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.is_err());
|
||||
|
||||
@@ -1401,7 +1390,7 @@ async fn test_invite_access(
|
||||
.unwrap();
|
||||
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1487,7 +1476,7 @@ async fn test_channel_moving(
|
||||
struct ExpectedChannel {
|
||||
depth: usize,
|
||||
id: ChannelId,
|
||||
name: String,
|
||||
name: SharedString,
|
||||
role: ChannelRole,
|
||||
}
|
||||
|
||||
@@ -1526,7 +1515,7 @@ fn assert_channels(
|
||||
.ordered_channels()
|
||||
.map(|(depth, channel)| ExpectedChannel {
|
||||
depth,
|
||||
name: channel.name.clone(),
|
||||
name: channel.name.clone().into(),
|
||||
id: channel.id,
|
||||
role: channel.role,
|
||||
})
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
// use call::ActiveCall;
|
||||
// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
|
||||
// use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
// use gpui::{BackgroundExecutor, TestAppContext, View};
|
||||
// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext};
|
||||
// use live_kit_client::MacOSDisplay;
|
||||
// use project::project_settings::ProjectSettings;
|
||||
// use rpc::proto::PeerId;
|
||||
// use serde_json::json;
|
||||
// use settings::SettingsStore;
|
||||
// use std::borrow::Cow;
|
||||
// use workspace::{
|
||||
// dock::{test::TestPanel, DockPosition},
|
||||
@@ -24,7 +26,7 @@
|
||||
// cx_c: &mut TestAppContext,
|
||||
// cx_d: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// 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;
|
||||
// let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -71,12 +73,22 @@
|
||||
// .unwrap();
|
||||
|
||||
// let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
// let workspace_a = window_a.root(cx_a);
|
||||
// let workspace_a = window_a.root(cx_a).unwrap();
|
||||
// let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
// let workspace_b = window_b.root(cx_b);
|
||||
// let workspace_b = window_b.root(cx_b).unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c);
|
||||
// let cx_c = &mut cx_c;
|
||||
// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d);
|
||||
// let cx_d = &mut cx_d;
|
||||
|
||||
// // Client A opens some editors.
|
||||
// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let editor_a1 = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -132,8 +144,8 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// cx_c.executor().run_until_parked();
|
||||
// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -145,19 +157,19 @@
|
||||
// Some((worktree_id, "2.txt").into())
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![2..1]
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![3..2]
|
||||
// );
|
||||
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// let active_call_c = cx_c.read(ActiveCall::global);
|
||||
// let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
// let window_c = client_c.build_workspace(&project_c, cx_c);
|
||||
// let workspace_c = window_c.root(cx_c);
|
||||
// let workspace_c = window_c.root(cx_c).unwrap();
|
||||
// active_call_c
|
||||
// .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
||||
// .await
|
||||
@@ -172,10 +184,13 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// cx_d.foreground().run_until_parked();
|
||||
// cx_d.executor().run_until_parked();
|
||||
// let active_call_d = cx_d.read(ActiveCall::global);
|
||||
// let project_d = client_d.build_remote_project(project_id, cx_d).await;
|
||||
// let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
|
||||
// let workspace_d = client_d
|
||||
// .build_workspace(&project_d, cx_d)
|
||||
// .root(cx_d)
|
||||
// .unwrap();
|
||||
// active_call_d
|
||||
// .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
|
||||
// .await
|
||||
@@ -183,7 +198,7 @@
|
||||
// drop(project_d);
|
||||
|
||||
// // All clients see that clients B and C are following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -198,7 +213,7 @@
|
||||
// });
|
||||
|
||||
// // All clients see that clients B is following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -216,7 +231,7 @@
|
||||
// .unwrap();
|
||||
|
||||
// // All clients see that clients B and C are following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -240,7 +255,7 @@
|
||||
// .unwrap();
|
||||
|
||||
// // All clients see that D is following C
|
||||
// cx_d.foreground().run_until_parked();
|
||||
// cx_d.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -257,7 +272,7 @@
|
||||
// cx_c.drop_last(workspace_c);
|
||||
|
||||
// // Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -271,12 +286,15 @@
|
||||
// workspace.activate_item(&editor_a1, cx)
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// // When client A opens a multibuffer, client B does so as well.
|
||||
// let multibuffer_a = cx_a.add_model(|cx| {
|
||||
// let multibuffer_a = cx_a.build_model(|cx| {
|
||||
// let buffer_a1 = project_a.update(cx, |project, cx| {
|
||||
// project
|
||||
// .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
|
||||
@@ -308,12 +326,12 @@
|
||||
// });
|
||||
// let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
|
||||
// let editor =
|
||||
// cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
|
||||
// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
|
||||
// workspace.add_item(Box::new(editor.clone()), cx);
|
||||
// editor
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -321,8 +339,8 @@
|
||||
// .unwrap()
|
||||
// });
|
||||
// assert_eq!(
|
||||
// multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
|
||||
// );
|
||||
|
||||
// // When client A navigates back and forth, client B does so as well.
|
||||
@@ -333,8 +351,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// workspace_a
|
||||
@@ -344,8 +365,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b2.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// workspace_a
|
||||
@@ -355,8 +379,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// // Changes to client A's editor are reflected on client B.
|
||||
@@ -364,20 +391,20 @@
|
||||
// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| {
|
||||
// editor_b1.update(cx_b, |editor, cx| {
|
||||
// assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
|
||||
// });
|
||||
|
||||
// editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
|
||||
// editor_a1.update(cx_a, |editor, cx| {
|
||||
// editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
|
||||
// editor.set_scroll_position(vec2f(0., 100.), cx);
|
||||
// editor.set_scroll_position(point(0., 100.), cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| {
|
||||
// editor_b1.update(cx_b, |editor, cx| {
|
||||
// assert_eq!(editor.selections.ranges(cx), &[3..3]);
|
||||
// });
|
||||
|
||||
@@ -390,11 +417,11 @@
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| workspace
|
||||
// workspace_b.update(cx_b, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// editor_b1.id()
|
||||
// .item_id()),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
|
||||
// // Client A starts following client B.
|
||||
@@ -405,15 +432,15 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// Some(peer_id_b)
|
||||
// );
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// editor_a1.id()
|
||||
// .item_id()),
|
||||
// editor_a1.item_id()
|
||||
// );
|
||||
|
||||
// // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
|
||||
@@ -432,7 +459,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .expect("no active item")
|
||||
@@ -446,8 +473,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_a1.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
|
||||
@@ -455,26 +485,26 @@
|
||||
// workspace.activate_item(&multibuffer_editor_b, cx)
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().id(),
|
||||
// multibuffer_editor_a.id()
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// multibuffer_editor_a.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
|
||||
// let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
|
||||
// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left));
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace.add_panel(panel, cx);
|
||||
// workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// shared_screen.id()
|
||||
// .item_id()),
|
||||
// shared_screen.item_id()
|
||||
// );
|
||||
|
||||
// // Toggling the focus back to the pane causes client A to return to the multibuffer.
|
||||
@@ -482,16 +512,16 @@
|
||||
// workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().id(),
|
||||
// multibuffer_editor_a.id()
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// multibuffer_editor_a.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates an item that doesn't implement following,
|
||||
// // so the previously-opened screen-sharing item gets activated.
|
||||
// let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
|
||||
// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace.active_pane().update(cx, |pane, cx| {
|
||||
// pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
|
||||
@@ -499,18 +529,18 @@
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// shared_screen.id()
|
||||
// .item_id()),
|
||||
// shared_screen.item_id()
|
||||
// );
|
||||
|
||||
// // Following interrupts when client B disconnects.
|
||||
// client_b.disconnect(&cx_b.to_async());
|
||||
// executor.advance_clock(RECONNECT_TIMEOUT);
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// None
|
||||
// );
|
||||
// }
|
||||
@@ -521,7 +551,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// 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;
|
||||
// server
|
||||
@@ -560,13 +590,19 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||
// let client_b_id = project_a.update(cx_a, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
|
||||
@@ -584,7 +620,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
|
||||
// let pane_paths = |pane: &View<workspace::Pane>, cx: &mut TestAppContext| {
|
||||
// pane.update(cx, |pane, cx| {
|
||||
// pane.items()
|
||||
// .map(|item| {
|
||||
@@ -642,7 +678,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// 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;
|
||||
// server
|
||||
@@ -685,7 +721,10 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client A opens a file.
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -696,7 +735,10 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client B opens a different file.
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// workspace_b
|
||||
// .update(cx_b, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
@@ -1167,7 +1209,7 @@
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// // 2 clients connect to a server.
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// 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;
|
||||
// server
|
||||
@@ -1207,8 +1249,17 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// // Client A opens some editors.
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let _editor_a1 = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -1219,9 +1270,12 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client B starts following client A.
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let leader_id = project_b.read_with(cx_b, |project, _| {
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let leader_id = project_b.update(cx_b, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
// workspace_b
|
||||
@@ -1231,10 +1285,10 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -1245,7 +1299,7 @@
|
||||
// // When client B moves, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1256,14 +1310,14 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// // When client B edits, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1274,16 +1328,16 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// // When client B scrolls, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| {
|
||||
// editor.set_scroll_position(vec2f(0., 3.), cx)
|
||||
// editor.set_scroll_position(point(0., 3.), cx)
|
||||
// });
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1294,7 +1348,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
@@ -1303,13 +1357,13 @@
|
||||
// workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
|
||||
// });
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
@@ -1321,7 +1375,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
// }
|
||||
@@ -1332,7 +1386,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// 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;
|
||||
// server
|
||||
@@ -1345,20 +1399,26 @@
|
||||
|
||||
// client_a.fs().insert_tree("/a", json!({})).await;
|
||||
// let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let project_id = active_call_a
|
||||
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// executor.run_until_parked();
|
||||
// let client_a_id = project_b.read_with(cx_b, |project, _| {
|
||||
// let client_a_id = project_b.update(cx_b, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
// let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||
// let client_b_id = project_a.update(cx_a, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
|
||||
@@ -1370,13 +1430,13 @@
|
||||
// });
|
||||
|
||||
// futures::try_join!(a_follow_b, b_follow_a).unwrap();
|
||||
// workspace_a.read_with(cx_a, |workspace, _| {
|
||||
// workspace_a.update(cx_a, |workspace, _| {
|
||||
// assert_eq!(
|
||||
// workspace.leader_for_pane(workspace.active_pane()),
|
||||
// Some(client_b_id)
|
||||
// );
|
||||
// });
|
||||
// workspace_b.read_with(cx_b, |workspace, _| {
|
||||
// workspace_b.update(cx_b, |workspace, _| {
|
||||
// assert_eq!(
|
||||
// workspace.leader_for_pane(workspace.active_pane()),
|
||||
// Some(client_a_id)
|
||||
@@ -1398,7 +1458,7 @@
|
||||
// // b opens a different file in project 2, a follows b
|
||||
// // b opens a different file in project 1, a cannot follow b
|
||||
// // b shares the project, a joins the project and follows b
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// 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;
|
||||
// cx_a.update(editor::init);
|
||||
@@ -1435,8 +1495,14 @@
|
||||
// let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
|
||||
// let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
|
||||
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
|
||||
// cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
|
||||
@@ -1455,6 +1521,12 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
|
||||
@@ -1476,11 +1548,12 @@
|
||||
// let workspace_b_project_a = cx_b
|
||||
// .windows()
|
||||
// .iter()
|
||||
// .max_by_key(|window| window.id())
|
||||
// .max_by_key(|window| window.item_id())
|
||||
// .unwrap()
|
||||
// .downcast::<Workspace>()
|
||||
// .unwrap()
|
||||
// .root(cx_b);
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// // assert that b is following a in project a in w.rs
|
||||
// workspace_b_project_a.update(cx_b, |workspace, cx| {
|
||||
@@ -1534,7 +1607,7 @@
|
||||
// workspace.leader_for_pane(workspace.active_pane())
|
||||
// );
|
||||
// let item = workspace.active_pane().read(cx).active_item().unwrap();
|
||||
// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
|
||||
// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into());
|
||||
// });
|
||||
|
||||
// // b moves to y.rs in b's project, a is still following but can't yet see
|
||||
@@ -1578,11 +1651,12 @@
|
||||
// let workspace_a_project_b = cx_a
|
||||
// .windows()
|
||||
// .iter()
|
||||
// .max_by_key(|window| window.id())
|
||||
// .max_by_key(|window| window.item_id())
|
||||
// .unwrap()
|
||||
// .downcast::<Workspace>()
|
||||
// .unwrap()
|
||||
// .root(cx_a);
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
|
||||
// workspace_a_project_b.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
|
||||
@@ -1596,12 +1670,151 @@
|
||||
// });
|
||||
// }
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_following_into_excluded_file(
|
||||
// executor: BackgroundExecutor,
|
||||
// mut cx_a: &mut TestAppContext,
|
||||
// mut cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// 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;
|
||||
// for cx in [&mut cx_a, &mut cx_b] {
|
||||
// cx.update(|cx| {
|
||||
// cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// server
|
||||
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
// .await;
|
||||
// let active_call_a = cx_a.read(ActiveCall::global);
|
||||
// let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// cx_a.update(editor::init);
|
||||
// cx_b.update(editor::init);
|
||||
|
||||
// client_a
|
||||
// .fs()
|
||||
// .insert_tree(
|
||||
// "/a",
|
||||
// json!({
|
||||
// ".git": {
|
||||
// "COMMIT_EDITMSG": "write your commit message here",
|
||||
// },
|
||||
// "1.txt": "one\none\none",
|
||||
// "2.txt": "two\ntwo\ntwo",
|
||||
// "3.txt": "three\nthree\nthree",
|
||||
// }),
|
||||
// )
|
||||
// .await;
|
||||
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
// active_call_a
|
||||
// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let project_id = active_call_a
|
||||
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
// active_call_b
|
||||
// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
// let workspace_a = window_a.root(cx_a).unwrap();
|
||||
// let peer_id_a = client_a.peer_id().unwrap();
|
||||
// let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
// let workspace_b = window_b.root(cx_b).unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// // Client A opens editors for a regular file and an excluded file.
|
||||
// let editor_for_regular = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap();
|
||||
// let editor_for_excluded_a = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap();
|
||||
|
||||
// // Client A updates their selections in those editors
|
||||
// editor_for_regular.update(cx_a, |editor, cx| {
|
||||
// editor.handle_input("a", cx);
|
||||
// editor.handle_input("b", cx);
|
||||
// editor.handle_input("c", cx);
|
||||
// editor.select_left(&Default::default(), cx);
|
||||
// assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
// });
|
||||
// editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
// editor.select_all(&Default::default(), cx);
|
||||
// editor.handle_input("new commit message", cx);
|
||||
// editor.select_left(&Default::default(), cx);
|
||||
// assert_eq!(editor.selections.ranges(cx), vec![18..17]);
|
||||
// });
|
||||
|
||||
// // When client B starts following client A, currently visible file is replicated
|
||||
// workspace_b
|
||||
// .update(cx_b, |workspace, cx| {
|
||||
// workspace.follow(peer_id_a, cx).unwrap()
|
||||
// })
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap()
|
||||
// });
|
||||
// assert_eq!(
|
||||
// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
|
||||
// Some((worktree_id, ".git/COMMIT_EDITMSG").into())
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![18..17]
|
||||
// );
|
||||
|
||||
// // 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 <b@b.b>", cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
// assert_eq!(
|
||||
// editor.text(cx),
|
||||
// "new commit messag\nCo-Authored-By: B <b@b.b>"
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// fn visible_push_notifications(
|
||||
// cx: &mut TestAppContext,
|
||||
// ) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
// ) -> Vec<gpui::View<ProjectSharedNotification>> {
|
||||
// let mut ret = Vec::new();
|
||||
// for window in cx.windows() {
|
||||
// window.read_with(cx, |window| {
|
||||
// window.update(cx, |window| {
|
||||
// if let Some(handle) = window
|
||||
// .root_view()
|
||||
// .clone()
|
||||
@@ -1645,8 +1858,8 @@
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
|
||||
// workspace.read_with(cx, |workspace, cx| {
|
||||
// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
|
||||
// workspace.update(cx, |workspace, cx| {
|
||||
// let active_pane = workspace.active_pane();
|
||||
// workspace
|
||||
// .panes()
|
||||
|
||||
@@ -510,10 +510,9 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||
|
||||
// Simultaneously join channel 1 and then channel 2
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_1, cx))
|
||||
.detach();
|
||||
let join_channel_2 =
|
||||
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
|
||||
let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
|
||||
|
||||
join_channel_2.await.unwrap();
|
||||
|
||||
@@ -539,8 +538,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||
call.invite(client_c.user_id().unwrap(), None, cx)
|
||||
});
|
||||
|
||||
let join_channel =
|
||||
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
|
||||
let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
|
||||
|
||||
b_invite.await.unwrap();
|
||||
c_invite.await.unwrap();
|
||||
@@ -569,8 +567,7 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||
.unwrap();
|
||||
|
||||
// Simultaneously join channel 1 and call user B and user C from client A.
|
||||
let join_channel =
|
||||
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
|
||||
let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
|
||||
|
||||
let b_invite = active_call_a.update(cx_a, |call, cx| {
|
||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||
@@ -1826,7 +1823,7 @@ async fn test_active_call_events(
|
||||
owner: Arc::new(User {
|
||||
id: client_a.user_id().unwrap(),
|
||||
github_login: "user_a".to_string(),
|
||||
avatar: None,
|
||||
avatar_uri: "avatar_a".into(),
|
||||
}),
|
||||
project_id: project_a_id,
|
||||
worktree_root_names: vec!["a".to_string()],
|
||||
@@ -1844,7 +1841,7 @@ async fn test_active_call_events(
|
||||
owner: Arc::new(User {
|
||||
id: client_b.user_id().unwrap(),
|
||||
github_login: "user_b".to_string(),
|
||||
avatar: None,
|
||||
avatar_uri: "avatar_b".into(),
|
||||
}),
|
||||
project_id: project_b_id,
|
||||
worktree_root_names: vec!["b".to_string()]
|
||||
@@ -2784,11 +2781,10 @@ async fn test_fs_operations(
|
||||
|
||||
let entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2815,8 +2811,8 @@ async fn test_fs_operations(
|
||||
.update(cx_b, |project, cx| {
|
||||
project.rename_entry(entry.id, Path::new("d.txt"), cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2841,11 +2837,10 @@ async fn test_fs_operations(
|
||||
|
||||
let dir_entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2870,27 +2865,24 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2931,11 +2923,10 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
.unwrap()
|
||||
project.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3688,7 +3679,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
&[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -3708,14 +3699,14 @@ async fn test_collaborating_with_diagnostics(
|
||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
let project_c_diagnostic_summaries =
|
||||
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>()
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
|
||||
})));
|
||||
project_c.update(cx_c, |_, cx| {
|
||||
let summaries = project_c_diagnostic_summaries.clone();
|
||||
cx.subscribe(&project_c, {
|
||||
move |p, _, event, cx| {
|
||||
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||
*summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
|
||||
*summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -3766,7 +3757,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -3783,7 +3774,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -3844,15 +3835,24 @@ async fn test_collaborating_with_diagnostics(
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::db::ChannelRole;
|
||||
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, SharedString, TestAppContext};
|
||||
use rand::prelude::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@@ -30,13 +30,13 @@ struct RandomChannelBufferTest;
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
enum ChannelBufferOperation {
|
||||
JoinChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
},
|
||||
LeaveChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
},
|
||||
EditChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
},
|
||||
Noop,
|
||||
|
||||
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
project
|
||||
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
|
||||
.unwrap()
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use client::{
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
@@ -209,7 +209,7 @@ impl TestServer {
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let mut language_registry = LanguageRegistry::test();
|
||||
language_registry.set_executor(cx.executor());
|
||||
@@ -221,7 +221,6 @@ impl TestServer {
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
call_factory: |_, _| Box::new(workspace::TestCallHandler),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
@@ -603,14 +602,12 @@ impl TestClient {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
//todo(workspace)
|
||||
#[allow(dead_code)]
|
||||
pub fn build_workspace(
|
||||
&self,
|
||||
pub fn build_workspace<'a>(
|
||||
&'a self,
|
||||
project: &Model<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> WindowHandle<Workspace> {
|
||||
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
# auto_update = { path = "../auto_update" }
|
||||
auto_update = { package = "auto_update2", path = "../auto_update2" }
|
||||
db = { package = "db2", path = "../db2" }
|
||||
call = { package = "call2", path = "../call2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
@@ -32,7 +32,7 @@ collections = { path = "../collections" }
|
||||
# context_menu = { path = "../context_menu" }
|
||||
# drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { package="editor2", path = "../editor2" }
|
||||
#feedback = { path = "../feedback" }
|
||||
feedback = { package = "feedback2", path = "../feedback2" }
|
||||
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
@@ -41,13 +41,13 @@ notifications = { package = "notifications2", path = "../notifications2" }
|
||||
rich_text = { package = "rich_text2", path = "../rich_text2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
# recent_projects = { path = "../recent_projects" }
|
||||
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
||||
rpc = { package ="rpc2", path = "../rpc2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
# theme_selector = { path = "../theme_selector" }
|
||||
# vcs_menu = { path = "../vcs_menu" }
|
||||
theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
|
||||
vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
@@ -1,454 +1,450 @@
|
||||
// use anyhow::{anyhow, Result};
|
||||
// use call::report_call_event_for_channel;
|
||||
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
// use client::{
|
||||
// proto::{self, PeerId},
|
||||
// Collaborator, ParticipantIndex,
|
||||
// };
|
||||
// use collections::HashMap;
|
||||
// use editor::{CollaborationHub, Editor};
|
||||
// use gpui::{
|
||||
// actions,
|
||||
// elements::{ChildView, Label},
|
||||
// geometry::vector::Vector2F,
|
||||
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
|
||||
// ViewContext, ViewHandle,
|
||||
// };
|
||||
// use project::Project;
|
||||
// use smallvec::SmallVec;
|
||||
// use std::{
|
||||
// any::{Any, TypeId},
|
||||
// sync::Arc,
|
||||
// };
|
||||
// use util::ResultExt;
|
||||
// use workspace::{
|
||||
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
// register_followable_item,
|
||||
// searchable::SearchableItemHandle,
|
||||
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
// };
|
||||
use anyhow::Result;
|
||||
use call::report_call_event_for_channel;
|
||||
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Collaborator, ParticipantIndex,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{CollaborationHub, Editor, EditorEvent};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
|
||||
IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::{prelude::*, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
// actions!(channel_view, [Deploy]);
|
||||
actions!(collab, [Deploy]);
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// register_followable_item::<ChannelView>(cx)
|
||||
// }
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
}
|
||||
|
||||
// pub struct ChannelView {
|
||||
// pub editor: ViewHandle<Editor>,
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// remote_id: Option<ViewId>,
|
||||
// _editor_event_subscription: Subscription,
|
||||
// }
|
||||
pub struct ChannelView {
|
||||
pub editor: View<Editor>,
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
remote_id: Option<ViewId>,
|
||||
_editor_event_subscription: Subscription,
|
||||
}
|
||||
|
||||
// impl ChannelView {
|
||||
// pub fn open(
|
||||
// channel_id: ChannelId,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let pane = workspace.read(cx).active_pane().clone();
|
||||
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_view = channel_view.await?;
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// report_call_event_for_channel(
|
||||
// "open channel notes",
|
||||
// channel_id,
|
||||
// &workspace.read(cx).app_state().client,
|
||||
// cx,
|
||||
// );
|
||||
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
// });
|
||||
// anyhow::Ok(channel_view)
|
||||
// })
|
||||
// }
|
||||
impl ChannelView {
|
||||
pub fn open(
|
||||
channel_id: ChannelId,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
channel_id,
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
})?;
|
||||
anyhow::Ok(channel_view)
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn open_in_pane(
|
||||
// channel_id: ChannelId,
|
||||
// pane: ViewHandle<Pane>,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let workspace = workspace.read(cx);
|
||||
// let project = workspace.project().to_owned();
|
||||
// let channel_store = ChannelStore::global(cx);
|
||||
// let language_registry = workspace.app_state().languages.clone();
|
||||
// let markdown = language_registry.language_for_name("Markdown");
|
||||
// let channel_buffer =
|
||||
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
pub fn open_in_pane(
|
||||
channel_id: ChannelId,
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let language_registry = workspace.app_state().languages.clone();
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
let channel_buffer =
|
||||
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_buffer = channel_buffer.await?;
|
||||
// let markdown = markdown.await.log_err();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
let markdown = markdown.await.log_err();
|
||||
|
||||
// channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
// buffer.buffer().update(cx, |buffer, cx| {
|
||||
// buffer.set_language_registry(language_registry);
|
||||
// if let Some(markdown) = markdown {
|
||||
// buffer.set_language(Some(markdown), cx);
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_language_registry(language_registry);
|
||||
if let Some(markdown) = markdown {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
// let existing_view = pane
|
||||
// .items_of_type::<Self>()
|
||||
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// // If this channel buffer is already open in this pane, just return it.
|
||||
// if let Some(existing_view) = existing_view.clone() {
|
||||
// if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
// return existing_view;
|
||||
// }
|
||||
// }
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
// let view = cx.add_view(|cx| {
|
||||
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
// this.acknowledge_buffer_version(cx);
|
||||
// this
|
||||
// });
|
||||
let view = cx.build_view(|cx| {
|
||||
let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
|
||||
// // If the pane contained a disconnected view for this channel buffer,
|
||||
// // replace that.
|
||||
// if let Some(existing_item) = existing_view {
|
||||
// if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
|
||||
// .detach();
|
||||
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
// }
|
||||
// }
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
// view
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("pane was dropped"))
|
||||
// })
|
||||
// }
|
||||
view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn new(
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Self {
|
||||
// let buffer = channel_buffer.read(cx).buffer();
|
||||
// let editor = cx.add_view(|cx| {
|
||||
// let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
// channel_buffer.clone(),
|
||||
// )));
|
||||
// editor.set_read_only(
|
||||
// !channel_buffer
|
||||
// .read(cx)
|
||||
// .channel(cx)
|
||||
// .is_some_and(|c| c.can_edit_notes()),
|
||||
// );
|
||||
// editor
|
||||
// });
|
||||
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = channel_buffer.read(cx).buffer();
|
||||
let editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
channel_buffer.clone(),
|
||||
)));
|
||||
editor.set_read_only(
|
||||
!channel_buffer
|
||||
.read(cx)
|
||||
.channel(cx)
|
||||
.is_some_and(|c| c.can_edit_notes()),
|
||||
);
|
||||
editor
|
||||
});
|
||||
let _editor_event_subscription =
|
||||
cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
|
||||
|
||||
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
// .detach();
|
||||
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
.detach();
|
||||
|
||||
// Self {
|
||||
// editor,
|
||||
// project,
|
||||
// channel_store,
|
||||
// channel_buffer,
|
||||
// remote_id: None,
|
||||
// _editor_event_subscription,
|
||||
// }
|
||||
// }
|
||||
Self {
|
||||
editor,
|
||||
project,
|
||||
channel_store,
|
||||
channel_buffer,
|
||||
remote_id: None,
|
||||
_editor_event_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
// self.channel_buffer.read(cx).channel(cx)
|
||||
// }
|
||||
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
self.channel_buffer.read(cx).channel(cx)
|
||||
}
|
||||
|
||||
// fn handle_channel_buffer_event(
|
||||
// &mut self,
|
||||
// _: ModelHandle<ChannelBuffer>,
|
||||
// event: &ChannelBufferEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// match event {
|
||||
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(true);
|
||||
// cx.notify();
|
||||
// }),
|
||||
// ChannelBufferEvent::ChannelChanged => {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
// cx.emit(editor::Event::TitleChanged);
|
||||
// cx.notify()
|
||||
// });
|
||||
// }
|
||||
// ChannelBufferEvent::BufferEdited => {
|
||||
// if cx.is_self_focused() || self.editor.is_focused(cx) {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// } else {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.notes_changed(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
// }
|
||||
// }
|
||||
fn handle_channel_buffer_event(
|
||||
&mut self,
|
||||
_: Model<ChannelBuffer>,
|
||||
event: &ChannelBufferEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(true);
|
||||
cx.notify();
|
||||
}),
|
||||
ChannelBufferEvent::ChannelChanged => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
cx.emit(editor::EditorEvent::TitleChanged);
|
||||
cx.notify()
|
||||
});
|
||||
}
|
||||
ChannelBufferEvent::BufferEdited => {
|
||||
if self.editor.read(cx).is_focused(cx) {
|
||||
self.acknowledge_buffer_version(cx);
|
||||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.notes_changed(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.acknowledge_notes_version(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// self.channel_buffer.update(cx, |buffer, cx| {
|
||||
// buffer.acknowledge_buffer_version(cx);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.acknowledge_notes_version(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.channel_buffer.update(cx, |buffer, cx| {
|
||||
buffer.acknowledge_buffer_version(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// impl Entity for ChannelView {
|
||||
// type Event = editor::Event;
|
||||
// }
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
|
||||
// impl View for ChannelView {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ChannelView"
|
||||
// }
|
||||
impl Render for ChannelView {
|
||||
type Element = AnyView;
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// ChildView::new(self.editor.as_any(), cx).into_any()
|
||||
// }
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.editor.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// if cx.is_self_focused() {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// cx.focus(self.editor.as_any())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl FocusableView for ChannelView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// impl Item for ChannelView {
|
||||
// fn act_as_type<'a>(
|
||||
// &'a self,
|
||||
// type_id: TypeId,
|
||||
// self_handle: &'a ViewHandle<Self>,
|
||||
// _: &'a AppContext,
|
||||
// ) -> Option<&'a AnyViewHandle> {
|
||||
// if type_id == TypeId::of::<Self>() {
|
||||
// Some(self_handle)
|
||||
// } else if type_id == TypeId::of::<Editor>() {
|
||||
// Some(&self.editor)
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
impl Item for ChannelView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
// fn tab_content<V: 'static>(
|
||||
// &self,
|
||||
// _: Option<usize>,
|
||||
// style: &theme::Tab,
|
||||
// cx: &gpui::AppContext,
|
||||
// ) -> AnyElement<V> {
|
||||
// let label = if let Some(channel) = self.channel(cx) {
|
||||
// match (
|
||||
// channel.can_edit_notes(),
|
||||
// self.channel_buffer.read(cx).is_connected(),
|
||||
// ) {
|
||||
// (true, true) => format!("#{}", channel.name),
|
||||
// (false, true) => format!("#{} (read-only)", channel.name),
|
||||
// (_, false) => format!("#{} (disconnected)", channel.name),
|
||||
// }
|
||||
// } else {
|
||||
// format!("channel notes (disconnected)")
|
||||
// };
|
||||
// Label::new(label, style.label.to_owned()).into_any()
|
||||
// }
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a View<Self>,
|
||||
_: &'a AppContext,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||
// Some(Self::new(
|
||||
// self.project.clone(),
|
||||
// self.channel_store.clone(),
|
||||
// self.channel_buffer.clone(),
|
||||
// cx,
|
||||
// ))
|
||||
// }
|
||||
fn tab_content(&self, _: Option<usize>, selected: bool, cx: &WindowContext) -> AnyElement {
|
||||
let label = if let Some(channel) = self.channel(cx) {
|
||||
match (
|
||||
channel.can_edit_notes(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(true, true) => format!("#{}", channel.name),
|
||||
(false, true) => format!("#{} (read-only)", channel.name),
|
||||
(_, false) => format!("#{} (disconnected)", channel.name),
|
||||
}
|
||||
} else {
|
||||
format!("channel notes (disconnected)")
|
||||
};
|
||||
Label::new(label)
|
||||
.color(if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
// fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
|
||||
Some(cx.build_view(|cx| {
|
||||
Self::new(
|
||||
self.project.clone(),
|
||||
self.channel_store.clone(),
|
||||
self.channel_buffer.clone(),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
// }
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
// }
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
// }
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
}
|
||||
|
||||
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
// Some(Box::new(self.editor.clone()))
|
||||
// }
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
}
|
||||
|
||||
// fn show_toolbar(&self) -> bool {
|
||||
// true
|
||||
// }
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
|
||||
// self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
// }
|
||||
fn show_toolbar(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
// editor::Editor::to_item_events(event)
|
||||
// }
|
||||
// }
|
||||
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
|
||||
self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
}
|
||||
|
||||
// impl FollowableItem for ChannelView {
|
||||
// fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
// self.remote_id
|
||||
// }
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
}
|
||||
|
||||
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// if !channel_buffer.is_connected() {
|
||||
// return None;
|
||||
// }
|
||||
impl FollowableItem for ChannelView {
|
||||
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
// Some(proto::view::Variant::ChannelView(
|
||||
// proto::view::ChannelView {
|
||||
// channel_id: channel_buffer.channel_id,
|
||||
// editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
// self.editor.read(cx).to_state_proto(cx)
|
||||
// {
|
||||
// Some(proto)
|
||||
// } else {
|
||||
// None
|
||||
// },
|
||||
// },
|
||||
// ))
|
||||
// }
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
if !channel_buffer.is_connected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// fn from_state_proto(
|
||||
// pane: ViewHandle<workspace::Pane>,
|
||||
// workspace: ViewHandle<workspace::Workspace>,
|
||||
// remote_id: workspace::ViewId,
|
||||
// state: &mut Option<proto::view::Variant>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
|
||||
// let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
// return None;
|
||||
// };
|
||||
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
// unreachable!()
|
||||
// };
|
||||
Some(proto::view::Variant::ChannelView(
|
||||
proto::view::ChannelView {
|
||||
channel_id: channel_buffer.channel_id,
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
{
|
||||
Some(proto)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<workspace::Workspace>,
|
||||
remote_id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
|
||||
let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
return None;
|
||||
};
|
||||
let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// Some(cx.spawn(|mut cx| async move {
|
||||
// let this = open.await?;
|
||||
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
|
||||
// let task = this
|
||||
// .update(&mut cx, |this, cx| {
|
||||
// this.remote_id = Some(remote_id);
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
|
||||
// if let Some(state) = state.editor {
|
||||
// Some(this.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(
|
||||
// &this.project,
|
||||
// proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
// selections: state.selections,
|
||||
// pending_selection: state.pending_selection,
|
||||
// scroll_top_anchor: state.scroll_top_anchor,
|
||||
// scroll_x: state.scroll_x,
|
||||
// scroll_y: state.scroll_y,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// }))
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("window was closed"))?;
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
this.remote_id = Some(remote_id);
|
||||
|
||||
// if let Some(task) = task {
|
||||
// task.await?;
|
||||
// }
|
||||
if let Some(state) = state.editor {
|
||||
Some(this.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(
|
||||
&this.project,
|
||||
proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
selections: state.selections,
|
||||
pending_selection: state.pending_selection,
|
||||
scroll_top_anchor: state.scroll_top_anchor,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
// Ok(this)
|
||||
// }))
|
||||
// }
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
// fn add_event_to_update_proto(
|
||||
// &self,
|
||||
// event: &Self::Event,
|
||||
// update: &mut Option<proto::update_view::Variant>,
|
||||
// cx: &AppContext,
|
||||
// ) -> bool {
|
||||
// self.editor
|
||||
// .read(cx)
|
||||
// .add_event_to_update_proto(event, update, cx)
|
||||
// }
|
||||
Ok(this)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn apply_update_proto(
|
||||
// &mut self,
|
||||
// project: &ModelHandle<Project>,
|
||||
// message: proto::update_view::Variant,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> gpui::Task<anyhow::Result<()>> {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(project, message, cx)
|
||||
// })
|
||||
// }
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &EditorEvent,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.add_event_to_update_proto(event, update, cx)
|
||||
}
|
||||
|
||||
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
// })
|
||||
// }
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
project: &Model<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<anyhow::Result<()>> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(project, message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||
// Editor::should_unfollow_on_event(event, cx)
|
||||
// }
|
||||
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
// impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
// self.0.read(cx).collaborators()
|
||||
// }
|
||||
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
|
||||
|
||||
// fn user_participant_indices<'a>(
|
||||
// &self,
|
||||
// cx: &'a AppContext,
|
||||
// ) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
// self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
// }
|
||||
// }
|
||||
impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
self.0.read(cx).collaborators()
|
||||
}
|
||||
|
||||
fn user_participant_indices<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ use client::UserId;
|
||||
use collections::HashMap;
|
||||
use editor::{AnchorRangeExt, Editor};
|
||||
use gpui::{
|
||||
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyView, AsyncWindowContext, FocusableView, Model, Render, SharedString, Task, View,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use lazy_static::lazy_static;
|
||||
use project::search::SearchQuery;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use workspace::item::ItemHandle;
|
||||
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
@@ -19,8 +20,8 @@ lazy_static! {
|
||||
}
|
||||
|
||||
pub struct MessageEditor {
|
||||
pub editor: ViewHandle<Editor>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
pub editor: View<Editor>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
users: HashMap<String, UserId>,
|
||||
mentions: Vec<UserId>,
|
||||
mentions_task: Option<Task<()>>,
|
||||
@@ -30,8 +31,8 @@ pub struct MessageEditor {
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
editor: ViewHandle<Editor>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
editor: View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -48,15 +49,13 @@ impl MessageEditor {
|
||||
cx.subscribe(&buffer, Self::on_buffer_event).detach();
|
||||
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
});
|
||||
anyhow::Ok(())
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
editor,
|
||||
@@ -71,7 +70,7 @@ impl MessageEditor {
|
||||
pub fn set_channel(
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
channel_name: Option<String>,
|
||||
channel_name: Option<SharedString>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
@@ -132,26 +131,28 @@ impl MessageEditor {
|
||||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
buffer: Model<Buffer>,
|
||||
event: &language::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let language::Event::Reparsed | language::Event::Edited = event {
|
||||
let buffer = buffer.read(cx).snapshot();
|
||||
self.mentions_task = Some(cx.spawn(|this, cx| async move {
|
||||
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
|
||||
cx.background_executor()
|
||||
.timer(MENTIONS_DEBOUNCE_INTERVAL)
|
||||
.await;
|
||||
Self::find_mentions(this, buffer, cx).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_mentions(
|
||||
this: WeakViewHandle<MessageEditor>,
|
||||
this: WeakView<MessageEditor>,
|
||||
buffer: BufferSnapshot,
|
||||
mut cx: AsyncAppContext,
|
||||
mut cx: AsyncWindowContext,
|
||||
) {
|
||||
let (buffer, ranges) = cx
|
||||
.background()
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
|
||||
(buffer, ranges)
|
||||
@@ -180,11 +181,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
editor.clear_highlights::<Self>(cx);
|
||||
editor.highlight_text::<Self>(
|
||||
anchor_ranges,
|
||||
theme::current(cx).chat_panel.rich_text.mention_highlight,
|
||||
cx,
|
||||
)
|
||||
editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
|
||||
});
|
||||
|
||||
this.mentions = mentioned_user_ids;
|
||||
@@ -192,21 +189,17 @@ impl MessageEditor {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for MessageEditor {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.editor);
|
||||
}
|
||||
impl Render for MessageEditor {
|
||||
type Element = AnyView;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.editor.to_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +207,7 @@ impl View for MessageEditor {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use gpui::{TestAppContext, WindowHandle};
|
||||
use gpui::{Context as _, TestAppContext, VisualContext as _};
|
||||
use language::{Language, LanguageConfig};
|
||||
use rpc::proto;
|
||||
use settings::SettingsStore;
|
||||
@@ -222,8 +215,17 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_editor(cx: &mut TestAppContext) {
|
||||
let editor = init_test(cx);
|
||||
let editor = editor.root(cx);
|
||||
let language_registry = init_test(cx);
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|cx| {
|
||||
MessageEditor::new(
|
||||
language_registry,
|
||||
ChannelStore::global(cx),
|
||||
cx.build_view(|cx| Editor::auto_height(4, cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_members(
|
||||
@@ -232,7 +234,7 @@ mod tests {
|
||||
user: Arc::new(User {
|
||||
github_login: "a-b".into(),
|
||||
id: 101,
|
||||
avatar: None,
|
||||
avatar_uri: "avatar_a-b".into(),
|
||||
}),
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
role: proto::ChannelRole::Member,
|
||||
@@ -241,7 +243,7 @@ mod tests {
|
||||
user: Arc::new(User {
|
||||
github_login: "C_D".into(),
|
||||
id: 102,
|
||||
avatar: None,
|
||||
avatar_uri: "avatar_C_D".into(),
|
||||
}),
|
||||
kind: proto::channel_member::Kind::Member,
|
||||
role: proto::ChannelRole::Member,
|
||||
@@ -255,7 +257,7 @@ mod tests {
|
||||
});
|
||||
});
|
||||
|
||||
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||
cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
|
||||
@@ -269,15 +271,14 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
|
||||
cx.update(|cx| {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
client::init(&client, cx);
|
||||
@@ -292,16 +293,6 @@ mod tests {
|
||||
},
|
||||
Some(tree_sitter_markdown::language()),
|
||||
)));
|
||||
|
||||
let editor = cx.add_window(|cx| {
|
||||
MessageEditor::new(
|
||||
language_registry,
|
||||
ChannelStore::global(cx),
|
||||
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
editor
|
||||
language_registry
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,19 +3,17 @@ use client::{
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
User, UserId, UserStore,
|
||||
};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
actions, div, overlay, AppContext, ClipboardItem, DismissEvent, Div, EventEmitter,
|
||||
FocusableView, Model, ParentElement, Render, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, Avatar, Checkbox, ContextMenu, ListItem};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
use workspace::ModalView;
|
||||
|
||||
actions!(
|
||||
channel_modal,
|
||||
@@ -27,34 +25,27 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ChannelModalDelegate>::init(cx);
|
||||
cx.add_action(ChannelModal::toggle_mode);
|
||||
cx.add_action(ChannelModal::toggle_member_admin);
|
||||
cx.add_action(ChannelModal::remove_member);
|
||||
cx.add_action(ChannelModal::dismiss);
|
||||
}
|
||||
|
||||
pub struct ChannelModal {
|
||||
picker: ViewHandle<Picker<ChannelModalDelegate>>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
picker: View<Picker<ChannelModalDelegate>>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ChannelModal {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
mode: Mode,
|
||||
members: Vec<ChannelMembership>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
let picker = cx.add_view(|cx| {
|
||||
let channel_modal = cx.view().downgrade();
|
||||
let picker = cx.build_view(|cx| {
|
||||
Picker::new(
|
||||
ChannelModalDelegate {
|
||||
channel_modal,
|
||||
matching_users: Vec::new(),
|
||||
matching_member_indices: Vec::new(),
|
||||
selected_index: 0,
|
||||
@@ -62,33 +53,24 @@ impl ChannelModal {
|
||||
channel_store: channel_store.clone(),
|
||||
channel_id,
|
||||
match_candidates: Vec::new(),
|
||||
context_menu: None,
|
||||
members,
|
||||
mode,
|
||||
context_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
.modal(false)
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
let has_focus = picker.read(cx).has_focus();
|
||||
|
||||
Self {
|
||||
picker,
|
||||
channel_store,
|
||||
channel_id,
|
||||
has_focus,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||
let mode = match self.picker.read(cx).delegate().mode {
|
||||
let mode = match self.picker.read(cx).delegate.mode {
|
||||
Mode::ManageMembers => Mode::InviteMembers,
|
||||
Mode::InviteMembers => Mode::ManageMembers,
|
||||
};
|
||||
@@ -103,20 +85,20 @@ impl ChannelModal {
|
||||
let mut members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||
.update(cx, |picker, _| picker.delegate.members = members);
|
||||
})?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
@@ -129,204 +111,120 @@ impl ChannelModal {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().toggle_selected_member_admin(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().remove_selected_member(cx);
|
||||
fn set_channel_visiblity(&mut self, selection: &Selection, cx: &mut ViewContext<Self>) {
|
||||
self.channel_store.update(cx, |channel_store, cx| {
|
||||
channel_store
|
||||
.set_channel_visibility(
|
||||
self.channel_id,
|
||||
match selection {
|
||||
Selection::Unselected => ChannelVisibility::Members,
|
||||
Selection::Selected => ChannelVisibility::Public,
|
||||
Selection::Indeterminate => return,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChannelModal {
|
||||
type Event = PickerEvent;
|
||||
impl EventEmitter<DismissEvent> for ChannelModal {}
|
||||
impl ModalView for ChannelModal {}
|
||||
|
||||
impl FocusableView for ChannelModal {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl View for ChannelModal {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChannelModal"
|
||||
}
|
||||
impl Render for ChannelModal {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
let mode = self.picker.read(cx).delegate().mode;
|
||||
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||
return Empty::new().into_any();
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let channel_store = self.channel_store.read(cx);
|
||||
let Some(channel) = channel_store.channel_for_id(self.channel_id) else {
|
||||
return div();
|
||||
};
|
||||
let channel_name = channel.name.clone();
|
||||
let channel_id = channel.id;
|
||||
let visibility = channel.visibility;
|
||||
let mode = self.picker.read(cx).delegate.mode;
|
||||
|
||||
enum InviteMembers {}
|
||||
enum ManageMembers {}
|
||||
|
||||
fn render_mode_button<T: 'static>(
|
||||
mode: Mode,
|
||||
text: &'static str,
|
||||
current_mode: Mode,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
let active = mode == current_mode;
|
||||
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
let contained_text = theme.tab_button.style_for(active, state);
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if !active {
|
||||
this.set_mode(mode, cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_visibility(
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
enum TogglePublic {}
|
||||
|
||||
if visibility == ChannelVisibility::Members {
|
||||
return Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
v_stack()
|
||||
.key_context("ChannelModal")
|
||||
.on_action(cx.listener(Self::toggle_mode))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.child(
|
||||
v_stack()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.rounded_t(px(8.))
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(IconElement::new(Icon::Hash).size(IconSize::Medium))
|
||||
.child(Label::new(channel_name))
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(
|
||||
Checkbox::new(
|
||||
"is-public",
|
||||
if visibility == ChannelVisibility::Public {
|
||||
ui::Selection::Selected
|
||||
} else {
|
||||
ui::Selection::Unselected
|
||||
},
|
||||
)
|
||||
.on_click(cx.listener(Self::set_channel_visiblity)),
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
.child(Label::new("Public")),
|
||||
)
|
||||
.children(if visibility == ChannelVisibility::Public {
|
||||
Some(Button::new("copy-link", "Copy Link").on_click(cx.listener(
|
||||
move |this, _, cx| {
|
||||
if let Some(channel) =
|
||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
{
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
},
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.with_spacing(14.0)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
let style = theme.channel_link.style_for(state);
|
||||
Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(channel) =
|
||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
{
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
.with_child(Flex::row().with_children([
|
||||
render_mode_button::<InviteMembers>(
|
||||
Mode::InviteMembers,
|
||||
"Invite members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
render_mode_button::<ManageMembers>(
|
||||
Mode::ManageMembers,
|
||||
"Manage members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.child(
|
||||
Button::new("manage-members", "Manage Members")
|
||||
.selected(mode == Mode::ManageMembers)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.set_mode(Mode::ManageMembers, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("invite-members", "Invite Members")
|
||||
.selected(mode == Mode::InviteMembers)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.set_mode(Mode::InviteMembers, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ChannelModal {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
.child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,19 +235,22 @@ pub enum Mode {
|
||||
}
|
||||
|
||||
pub struct ChannelModalDelegate {
|
||||
channel_modal: WeakView<ChannelModal>,
|
||||
matching_users: Vec<Arc<User>>,
|
||||
matching_member_indices: Vec<usize>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
selected_index: usize,
|
||||
mode: Mode,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
context_menu: Option<(View<ContextMenu>, Subscription)>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ChannelModalDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
@@ -382,19 +283,19 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background().block(match_strings(
|
||||
let matches = cx.background_executor().block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background().clone(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matching_member_indices.clear();
|
||||
delegate
|
||||
.matching_member_indices
|
||||
@@ -412,8 +313,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
async {
|
||||
let users = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_users = users;
|
||||
picker.delegate.matching_users = users;
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
@@ -429,11 +329,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => {
|
||||
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
|
||||
self.show_context_menu(selected_user, role.unwrap_or(ChannelRole::Member), cx)
|
||||
}
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_selected_member(cx);
|
||||
self.remove_member(selected_user.id, cx);
|
||||
}
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||
self.invite_member(selected_user, cx)
|
||||
@@ -445,138 +345,69 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
if self.context_menu.is_none() {
|
||||
self.channel_modal
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, role) = self.user_at_index(ix).unwrap();
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let (user, role) = self.user_at_index(ix)?;
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
|
||||
let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
|
||||
let mut result = Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children({
|
||||
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
|| {
|
||||
Label::new("Invited", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
Some(
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
Some(
|
||||
Label::new("Guest", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
Svg::new("icons/ellipsis.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Mode::InviteMembers => match request_status {
|
||||
Some(proto::channel_member::Kind::Member) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.invitee_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.button_width)
|
||||
.with_height(theme.invitee_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.invitee_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
},
|
||||
};
|
||||
|
||||
svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
})
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any();
|
||||
|
||||
if selected {
|
||||
result = Stack::new()
|
||||
.with_child(result)
|
||||
.with_child(
|
||||
ChildView::new(&self.context_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right(),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
result
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.end_slot(h_stack().gap_2().map(|slot| {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => slot
|
||||
.children(
|
||||
if request_status == Some(proto::channel_member::Kind::Invitee) {
|
||||
Some(Label::new("Invited"))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
)
|
||||
.children(match role {
|
||||
Some(ChannelRole::Admin) => Some(Label::new("Admin")),
|
||||
Some(ChannelRole::Guest) => Some(Label::new("Guest")),
|
||||
_ => None,
|
||||
})
|
||||
.child(IconButton::new("ellipsis", Icon::Ellipsis))
|
||||
.children(
|
||||
if let (Some((menu, _)), true) = (&self.context_menu, selected) {
|
||||
Some(
|
||||
overlay()
|
||||
.anchor(gpui::AnchorCorner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
),
|
||||
Mode::InviteMembers => match request_status {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
slot.children(Some(Label::new("Invited")))
|
||||
}
|
||||
Some(proto::channel_member::Kind::Member) => {
|
||||
slot.children(Some(Label::new("Member")))
|
||||
}
|
||||
_ => slot,
|
||||
},
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,21 +441,20 @@ impl ChannelModalDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, role) = self.user_at_index(self.selected_index)?;
|
||||
let new_role = if role == Some(ChannelRole::Admin) {
|
||||
ChannelRole::Member
|
||||
} else {
|
||||
ChannelRole::Admin
|
||||
};
|
||||
fn set_user_role(
|
||||
&mut self,
|
||||
user_id: UserId,
|
||||
new_role: ChannelRole,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<()> {
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_role(self.channel_id, user.id, new_role, cx)
|
||||
store.set_member_role(self.channel_id, user_id, new_role, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user_id) {
|
||||
member.role = new_role;
|
||||
}
|
||||
cx.focus_self();
|
||||
@@ -635,16 +465,14 @@ impl ChannelModalDelegate {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, _) = self.user_at_index(self.selected_index)?;
|
||||
let user_id = user.id;
|
||||
fn remove_member(&mut self, user_id: UserId, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.remove_member(self.channel_id, user_id, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||
this.members.remove(ix);
|
||||
this.matching_member_indices.retain_mut(|member_ix| {
|
||||
@@ -661,7 +489,7 @@ impl ChannelModalDelegate {
|
||||
.selected_index
|
||||
.min(this.matching_member_indices.len().saturating_sub(1));
|
||||
|
||||
cx.focus_self();
|
||||
picker.focus(cx);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
@@ -683,7 +511,7 @@ impl ChannelModalDelegate {
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
role: ChannelRole::Member,
|
||||
};
|
||||
let members = &mut this.delegate_mut().members;
|
||||
let members = &mut this.delegate.members;
|
||||
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||
}
|
||||
@@ -694,24 +522,55 @@ impl ChannelModalDelegate {
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if role == ChannelRole::Admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
},
|
||||
ToggleMemberAdmin,
|
||||
),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
fn show_context_menu(
|
||||
&mut self,
|
||||
user: Arc<User>,
|
||||
role: ChannelRole,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) {
|
||||
let user_id = user.id;
|
||||
let picker = cx.view().clone();
|
||||
let context_menu = ContextMenu::build(cx, |mut menu, _cx| {
|
||||
menu = menu.entry("Remove Member", {
|
||||
let picker = picker.clone();
|
||||
move |cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker.delegate.remove_member(user_id, cx);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
let picker = picker.clone();
|
||||
match role {
|
||||
ChannelRole::Admin => {
|
||||
menu = menu.entry("Revoke Admin", move |cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_user_role(user_id, ChannelRole::Member, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
ChannelRole::Member => {
|
||||
menu = menu.entry("Make Admin", move |cx| {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker
|
||||
.delegate
|
||||
.set_user_role(user_id, ChannelRole::Admin, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
menu
|
||||
});
|
||||
cx.focus_view(&context_menu);
|
||||
let subscription = cx.subscribe(&context_menu, |picker, _, _: &DismissEvent, cx| {
|
||||
picker.delegate.context_menu = None;
|
||||
picker.focus(cx);
|
||||
cx.notify();
|
||||
});
|
||||
self.context_menu = Some((context_menu, subscription));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(unused)]
|
||||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{
|
||||
div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle,
|
||||
@@ -7,17 +8,12 @@ use gpui::{
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::{h_stack, v_stack, Label};
|
||||
use ui::{prelude::*, Avatar, ListItem};
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
//Picker::<ContactFinderDelegate>::init(cx);
|
||||
//cx.add_action(ContactFinder::dismiss)
|
||||
}
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct ContactFinder {
|
||||
picker: View<Picker<ContactFinderDelegate>>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ContactFinder {
|
||||
@@ -28,65 +24,39 @@ impl ContactFinder {
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
};
|
||||
let picker = cx.build_view(|cx| Picker::new(delegate, cx));
|
||||
let picker = cx.build_view(|cx| Picker::new(delegate, cx).modal(false));
|
||||
|
||||
Self {
|
||||
picker,
|
||||
has_focus: false,
|
||||
}
|
||||
Self { picker }
|
||||
}
|
||||
|
||||
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
// todo!()
|
||||
// picker.set_query(query, cx);
|
||||
picker.set_query(query, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ContactFinder {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render_mode_button(text: &'static str) -> AnyElement {
|
||||
Label::new(text).into_any_element()
|
||||
}
|
||||
|
||||
v_stack()
|
||||
.elevation_3(cx)
|
||||
.child(
|
||||
v_stack()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
// HACK: Prevent the background color from overflowing the parent container.
|
||||
.rounded_t(px(8.))
|
||||
.child(Label::new("Contacts"))
|
||||
.child(h_stack().children([render_mode_button("Invite new contacts")]))
|
||||
.bg(cx.theme().colors().element_background),
|
||||
.child(h_stack().child(Label::new("Invite new contacts"))),
|
||||
)
|
||||
.child(self.picker.clone())
|
||||
.w_96()
|
||||
.w(rems(34.))
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if cx.is_self_focused() {
|
||||
// cx.focus(&self.picker)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
|
||||
type Element = Div;
|
||||
}
|
||||
|
||||
// impl Modal for ContactFinder {
|
||||
// fn has_focus(&self) -> bool {
|
||||
// self.has_focus
|
||||
// }
|
||||
|
||||
// fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => true,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
parent: WeakView<ContactFinder>,
|
||||
potential_contacts: Arc<[Arc<User>]>,
|
||||
@@ -95,6 +65,7 @@ pub struct ContactFinderDelegate {
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ContactFinder {}
|
||||
impl ModalView for ContactFinder {}
|
||||
|
||||
impl FocusableView for ContactFinder {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
@@ -103,7 +74,8 @@ impl FocusableView for ContactFinder {
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContactFinderDelegate {
|
||||
type ListItem = Div;
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.potential_contacts.len()
|
||||
}
|
||||
@@ -159,9 +131,8 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
//cx.emit(PickerEvent::Dismiss);
|
||||
self.parent
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -181,47 +152,15 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
||||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
dbg!(icon_path);
|
||||
Some(
|
||||
div()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.children(user.avatar.clone().map(|avatar| img().data(avatar)))
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.selected(selected)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.children(icon_path.map(|icon_path| svg().path(icon_path))),
|
||||
.end_slot::<IconElement>(
|
||||
icon_path.map(|icon_path| IconElement::from_path(icon_path)),
|
||||
),
|
||||
)
|
||||
// Flex::row()
|
||||
// .with_children(user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.contact_avatar)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Label::new(user.github_login.clone(), style.label.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.contact_username)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_children(icon_path.map(|icon_path| {
|
||||
// Svg::new(icon_path)
|
||||
// .with_color(button_style.color)
|
||||
// .constrained()
|
||||
// .with_width(button_style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(button_style.container)
|
||||
// .constrained()
|
||||
// .with_width(button_style.button_width)
|
||||
// .with_height(button_style.button_width)
|
||||
// .aligned()
|
||||
// .flex_float()
|
||||
// }))
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_height(tabbed_modal.row_height)
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,32 +9,37 @@ mod panel_settings;
|
||||
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use call::{report_call_event_for_room, ActiveCall, Room};
|
||||
pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||
use gpui::{
|
||||
point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind,
|
||||
WindowOptions,
|
||||
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
|
||||
WindowKind, WindowOptions,
|
||||
};
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use settings::Settings;
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
// actions!(
|
||||
// collab,
|
||||
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||
// );
|
||||
actions!(
|
||||
collab,
|
||||
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||
);
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
CollaborationPanelSettings::register(cx);
|
||||
ChatPanelSettings::register(cx);
|
||||
NotificationPanelSettings::register(cx);
|
||||
|
||||
// vcs_menu::init(cx);
|
||||
vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(cx);
|
||||
// chat_panel::init(cx);
|
||||
channel_view::init(cx);
|
||||
chat_panel::init(cx);
|
||||
notification_panel::init(cx);
|
||||
notifications::init(&app_state, cx);
|
||||
|
||||
// cx.add_global_action(toggle_screen_sharing);
|
||||
@@ -42,61 +47,61 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// cx.add_global_action(toggle_deafen);
|
||||
}
|
||||
|
||||
// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
// let call = ActiveCall::global(cx).read(cx);
|
||||
// if let Some(room) = call.room().cloned() {
|
||||
// let client = call.client();
|
||||
// let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
// if room.is_screen_sharing() {
|
||||
// report_call_event_for_room(
|
||||
// "disable screen share",
|
||||
// room.id(),
|
||||
// room.channel_id(),
|
||||
// &client,
|
||||
// cx,
|
||||
// );
|
||||
// Task::ready(room.unshare_screen(cx))
|
||||
// } else {
|
||||
// report_call_event_for_room(
|
||||
// "enable screen share",
|
||||
// room.id(),
|
||||
// room.channel_id(),
|
||||
// &client,
|
||||
// cx,
|
||||
// );
|
||||
// room.share_screen(cx)
|
||||
// }
|
||||
// });
|
||||
// toggle_screen_sharing.detach_and_log_err(cx);
|
||||
// }
|
||||
// }
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
let call = ActiveCall::global(cx).read(cx);
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
report_call_event_for_room(
|
||||
"disable screen share",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
report_call_event_for_room(
|
||||
"enable screen share",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
// let call = ActiveCall::global(cx).read(cx);
|
||||
// if let Some(room) = call.room().cloned() {
|
||||
// let client = call.client();
|
||||
// room.update(cx, |room, cx| {
|
||||
// let operation = if room.is_muted(cx) {
|
||||
// "enable microphone"
|
||||
// } else {
|
||||
// "disable microphone"
|
||||
// };
|
||||
// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
|
||||
pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
let call = ActiveCall::global(cx).read(cx);
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
room.update(cx, |room, cx| {
|
||||
let operation = if room.is_muted(cx) {
|
||||
"enable microphone"
|
||||
} else {
|
||||
"disable microphone"
|
||||
};
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
|
||||
|
||||
// room.toggle_mute(cx)
|
||||
// })
|
||||
// .map(|task| task.detach_and_log_err(cx))
|
||||
// .log_err();
|
||||
// }
|
||||
// }
|
||||
room.toggle_mute(cx)
|
||||
})
|
||||
.map(|task| task.detach_and_log_err(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
// room.update(cx, Room::toggle_deafen)
|
||||
// .map(|task| task.detach_and_log_err(cx))
|
||||
// .log_err();
|
||||
// }
|
||||
// }
|
||||
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
room.update(cx, Room::toggle_deafen)
|
||||
.map(|task| task.detach_and_log_err(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
@@ -157,6 +162,6 @@ fn notification_window_options(
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||
// }
|
||||
fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||
cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use gpui::{
|
||||
div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext,
|
||||
div, AnyElement, Div, ElementId, IntoElement, ParentElement, RenderOnce, Styled, WindowContext,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, IntoElement)]
|
||||
pub struct FacePile {
|
||||
pub faces: Vec<AnyElement>,
|
||||
pub faces: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl RenderOnce for FacePile {
|
||||
@@ -15,66 +16,17 @@ impl RenderOnce for FacePile {
|
||||
let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
|
||||
let isnt_last = ix < player_count - 1;
|
||||
|
||||
div().when(isnt_last, |div| div.neg_mr_1()).child(player)
|
||||
div()
|
||||
.z_index((player_count - ix) as u8)
|
||||
.when(isnt_last, |div| div.neg_mr_1())
|
||||
.child(player)
|
||||
});
|
||||
div().p_1().flex().items_center().children(player_list)
|
||||
}
|
||||
}
|
||||
|
||||
// impl Element for FacePile {
|
||||
// type State = ();
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// state: Option<Self::State>,
|
||||
// cx: &mut WindowContext,
|
||||
// ) -> (LayoutId, Self::State) {
|
||||
// let mut width = 0.;
|
||||
// let mut max_height = 0.;
|
||||
// let mut faces = Vec::with_capacity(self.faces.len());
|
||||
// for face in &mut self.faces {
|
||||
// let layout = face.layout(cx);
|
||||
// width += layout.x();
|
||||
// max_height = f32::max(max_height, layout.y());
|
||||
// faces.push(layout);
|
||||
// }
|
||||
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
// (cx.request_layout(&Style::default(), faces), ())
|
||||
// // (
|
||||
// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||
// // (),
|
||||
// // ))
|
||||
// }
|
||||
|
||||
// fn paint(
|
||||
// &mut self,
|
||||
// bounds: RectF,
|
||||
// visible_bounds: RectF,
|
||||
// _layout: &mut Self::LayoutState,
|
||||
// view: &mut V,
|
||||
// cx: &mut ViewContext<V>,
|
||||
// ) -> Self::PaintState {
|
||||
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
// let origin_y = bounds.upper_right().y();
|
||||
// let mut origin_x = bounds.upper_right().x();
|
||||
|
||||
// for face in self.faces.iter_mut().rev() {
|
||||
// let size = face.size();
|
||||
// origin_x -= size.x();
|
||||
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
|
||||
|
||||
// cx.scene().push_layer(None);
|
||||
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
|
||||
// cx.scene().pop_layer();
|
||||
// origin_x += self.overlap;
|
||||
// }
|
||||
|
||||
// ()
|
||||
// }
|
||||
// }
|
||||
|
||||
impl Extend<AnyElement> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
impl ParentElement for FacePile {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.faces
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::sync::Arc;
|
||||
use workspace::AppState;
|
||||
|
||||
pub mod incoming_call_notification;
|
||||
// pub mod project_shared_notification;
|
||||
pub mod project_shared_notification;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
incoming_call_notification::init(app_state, cx);
|
||||
//project_shared_notification::init(app_state, cx);
|
||||
project_shared_notification::init(app_state, cx);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ use crate::notification_window_options;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce,
|
||||
StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle,
|
||||
div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext,
|
||||
VisualContext as _, WindowHandle,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, v_stack, Avatar, Button, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
@@ -32,8 +33,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
height: px(64.),
|
||||
};
|
||||
|
||||
for window in unique_screens {
|
||||
let options = notification_window_options(window, window_size);
|
||||
for screen in unique_screens {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let window = cx
|
||||
.open_window(options, |cx| {
|
||||
cx.build_view(|_| {
|
||||
@@ -46,15 +47,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
.unwrap();
|
||||
notification_windows.push(window);
|
||||
}
|
||||
|
||||
// for screen in cx.platform().screens() {
|
||||
// let window = cx
|
||||
// .add_window(notification_window_options(screen, window_size), |_| {
|
||||
// IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
||||
// });
|
||||
|
||||
// notification_windows.push(window);
|
||||
// }
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -83,21 +75,22 @@ impl IncomingCallNotificationState {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
if accept {
|
||||
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||
let caller_user_id = self.call.calling_user.id;
|
||||
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||
let app_state = self.app_state.clone();
|
||||
let cx: &mut AppContext = cx;
|
||||
cx.spawn(|cx| async move {
|
||||
join.await?;
|
||||
if let Some(_project_id) = initial_project_id {
|
||||
cx.update(|_cx| {
|
||||
if let Some(_app_state) = app_state.upgrade() {
|
||||
// workspace::join_remote_project(
|
||||
// project_id,
|
||||
// caller_user_id,
|
||||
// app_state,
|
||||
// cx,
|
||||
// )
|
||||
// .detach_and_log_err(cx);
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
@@ -121,14 +114,7 @@ impl IncomingCallNotification {
|
||||
}
|
||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
h_stack()
|
||||
.children(
|
||||
self.state
|
||||
.call
|
||||
.calling_user
|
||||
.avatar
|
||||
.as_ref()
|
||||
.map(|avatar| Avatar::data(avatar.clone())),
|
||||
)
|
||||
.child(Avatar::new(self.state.call.calling_user.avatar_uri.clone()))
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(format!(
|
||||
@@ -137,125 +123,25 @@ impl IncomingCallNotification {
|
||||
)))
|
||||
.child(self.render_buttons(cx)),
|
||||
)
|
||||
// let theme = &theme::current(cx).incoming_call_notification;
|
||||
// let default_project = proto::ParticipantProject::default();
|
||||
// let initial_project = self
|
||||
// .call
|
||||
// .initial_project
|
||||
// .as_ref()
|
||||
// .unwrap_or(&default_project);
|
||||
// Flex::row()
|
||||
// .with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.caller_avatar)
|
||||
// .aligned()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// self.call.calling_user.github_login.clone(),
|
||||
// theme.caller_username.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.caller_username.container),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// format!(
|
||||
// "is sharing a project in Zed{}",
|
||||
// if initial_project.worktree_root_names.is_empty() {
|
||||
// ""
|
||||
// } else {
|
||||
// ":"
|
||||
// }
|
||||
// ),
|
||||
// theme.caller_message.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.caller_message.container),
|
||||
// )
|
||||
// .with_children(if initial_project.worktree_root_names.is_empty() {
|
||||
// None
|
||||
// } else {
|
||||
// Some(
|
||||
// Label::new(
|
||||
// initial_project.worktree_root_names.join(", "),
|
||||
// theme.worktree_roots.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.worktree_roots.container),
|
||||
// )
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(theme.caller_metadata)
|
||||
// .aligned(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.caller_container)
|
||||
// .flex(1., true)
|
||||
// .into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
h_stack()
|
||||
.child(Button::new("Accept").render(cx).bg(green()).on_click({
|
||||
.child(Button::new("accept", "Accept").render(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(true, cx)
|
||||
}))
|
||||
.child(Button::new("Decline").render(cx).bg(red()).on_click({
|
||||
.child(Button::new("decline", "Decline").render(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(false, cx)
|
||||
}))
|
||||
|
||||
// enum Accept {}
|
||||
// enum Decline {}
|
||||
|
||||
// let theme = theme::current(cx);
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||
// let theme = &theme.incoming_call_notification;
|
||||
// Label::new("Accept", theme.accept_button.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.accept_button.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||
// this.respond(true, cx);
|
||||
// })
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||
// let theme = &theme.incoming_call_notification;
|
||||
// Label::new("Decline", theme.decline_button.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.decline_button.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, this, cx| {
|
||||
// this.respond(false, cx);
|
||||
// })
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_width(theme.incoming_call_notification.button_width)
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for IncomingCallNotification {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().bg(red()).flex_none().child(self.render_caller(cx))
|
||||
// Flex::row()
|
||||
// .with_child()
|
||||
// .with_child(self.render_buttons(cx))
|
||||
// .contained()
|
||||
// .with_background_color(background)
|
||||
// .expanded()
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, View, ViewContext,
|
||||
img, px, AppContext, Div, ParentElement, Render, Size, Styled, ViewContext, VisualContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, Weak};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{h_stack, prelude::*, v_stack, Button, Label};
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
@@ -21,38 +21,54 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
project_id,
|
||||
worktree_root_names,
|
||||
} => {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||
let window_size = Size {
|
||||
width: px(400.),
|
||||
height: px(96.),
|
||||
};
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let window =
|
||||
cx.add_window(notification_window_options(screen, window_size), |_| {
|
||||
for screen in cx.displays() {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let window = cx.open_window(options, |cx| {
|
||||
cx.build_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
});
|
||||
})
|
||||
});
|
||||
notification_windows
|
||||
.entry(*project_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(window);
|
||||
}
|
||||
}
|
||||
|
||||
room::Event::RemoteProjectUnshared { project_id }
|
||||
| room::Event::RemoteProjectJoined { project_id }
|
||||
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
|
||||
if let Some(windows) = notification_windows.remove(&project_id) {
|
||||
for window in windows {
|
||||
window.remove(cx);
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
room::Event::Left => {
|
||||
for (_, windows) in notification_windows.drain() {
|
||||
for window in windows {
|
||||
window.remove(cx);
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,117 +117,70 @@ impl ProjectSharedNotification {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
Flex::row()
|
||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.owner_avatar)
|
||||
.aligned()
|
||||
}))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(
|
||||
self.owner.github_login.clone(),
|
||||
theme.owner_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_username.container),
|
||||
impl Render for ProjectSharedNotification {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
// TODO: Is there a better place for us to initialize the font?
|
||||
let (ui_font, ui_font_size) = {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
(
|
||||
theme_settings.ui_font.family.clone(),
|
||||
theme_settings.ui_font_size.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
cx.set_rem_size(ui_font_size);
|
||||
|
||||
h_stack()
|
||||
.font(ui_font)
|
||||
.text_ui()
|
||||
.justify_between()
|
||||
.size_full()
|
||||
.elevation_3(cx)
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(
|
||||
img(self.owner.avatar_uri.clone())
|
||||
.w_16()
|
||||
.h_16()
|
||||
.rounded_full(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format!(
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(self.owner.github_login.clone()))
|
||||
.child(Label::new(format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
),
|
||||
theme.message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.message.container),
|
||||
)
|
||||
.with_children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(
|
||||
self.worktree_root_names.join(", "),
|
||||
theme.worktree_roots.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.worktree_roots.container),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.owner_metadata)
|
||||
.aligned(),
|
||||
)))
|
||||
.children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Label::new(self.worktree_root_names.join(", ")))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_container)
|
||||
.flex(1., true)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Open {}
|
||||
enum Dismiss {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Open", theme.open_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.open_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
|
||||
.flex(1., true),
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Button::new("open", "Open").on_click(cx.listener(
|
||||
move |this, _event, cx| {
|
||||
this.join(cx);
|
||||
},
|
||||
)))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|
||||
move |this, _event, cx| {
|
||||
this.dismiss(cx);
|
||||
},
|
||||
))),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.dismiss_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.dismiss(cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(theme.project_shared_notification.button_width)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectSharedNotification {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for ProjectSharedNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"ProjectSharedNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||
let background = theme::current(cx).project_shared_notification.background;
|
||||
Flex::row()
|
||||
.with_child(self.render_owner(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.expanded()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ path = "src/collections.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["seahash"]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
seahash = { version = "4.1", optional = true }
|
||||
rustc-hash = "1.1"
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
#[cfg(feature = "test-support")]
|
||||
#[derive(Clone, Default)]
|
||||
pub struct DeterministicState;
|
||||
pub type HashMap<K, V> = FxHashMap<K, V>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
impl std::hash::BuildHasher for DeterministicState {
|
||||
type Hasher = seahash::SeaHasher;
|
||||
|
||||
fn build_hasher(&self) -> Self::Hasher {
|
||||
seahash::SeaHasher::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V, DeterministicState>;
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub type HashSet<T> = std::collections::HashSet<T, DeterministicState>;
|
||||
pub type HashSet<T> = FxHashSet<T>;
|
||||
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
@@ -23,11 +10,14 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
pub use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::any::TypeId;
|
||||
pub use std::collections::*;
|
||||
|
||||
// NEW TYPES
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CommandPaletteFilter {
|
||||
pub filtered_namespaces: HashSet<&'static str>,
|
||||
pub hidden_namespaces: HashSet<&'static str>,
|
||||
pub hidden_action_types: HashSet<TypeId>,
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let filtered = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
filter.filtered_namespaces.contains(action.namespace())
|
||||
filter.hidden_namespaces.contains(action.namespace())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
// Add namespace filter, and redeploy the palette
|
||||
cx.update(|cx| {
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("editor");
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
language = { package="language2", path = "../language2", features = ["test-support"] }
|
||||
project = { package="project2", path = "../project2", features = ["test-support"] }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
|
||||
serde_json.workspace = true
|
||||
workspace = { package="workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
ctor.workspace = true
|
||||
|
||||
@@ -7,25 +7,28 @@ use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
||||
use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
actions!(Toggle);
|
||||
actions!(command_palette, [Toggle]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.set_global(HitCounts::default());
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.observe_new_views(CommandPalette::register).detach();
|
||||
}
|
||||
|
||||
impl ModalView for CommandPalette {}
|
||||
|
||||
pub struct CommandPalette {
|
||||
picker: View<Picker<CommandPaletteDelegate>>,
|
||||
}
|
||||
@@ -47,16 +50,18 @@ impl CommandPalette {
|
||||
.available_actions()
|
||||
.into_iter()
|
||||
.filter_map(|action| {
|
||||
let name = gpui::remove_the_2(action.name());
|
||||
let name = action.name();
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
|
||||
if filter.is_some_and(|f| {
|
||||
f.hidden_namespaces.contains(namespace)
|
||||
|| f.hidden_action_types.contains(&action.type_id())
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Command {
|
||||
name: humanize_action_name(&name),
|
||||
action,
|
||||
keystrokes: vec![], // todo!()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -81,7 +86,7 @@ impl Render for CommandPalette {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().min_w_96().child(self.picker.clone())
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +101,7 @@ pub struct CommandInterceptResult {
|
||||
|
||||
pub struct CommandPaletteDelegate {
|
||||
command_palette: WeakView<CommandPalette>,
|
||||
all_commands: Vec<Command>,
|
||||
commands: Vec<Command>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_ix: usize,
|
||||
@@ -105,7 +111,6 @@ pub struct CommandPaletteDelegate {
|
||||
struct Command {
|
||||
name: String,
|
||||
action: Box<dyn Action>,
|
||||
keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
impl Clone for Command {
|
||||
@@ -113,7 +118,6 @@ impl Clone for Command {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
action: self.action.boxed_clone(),
|
||||
keystrokes: self.keystrokes.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +136,7 @@ impl CommandPaletteDelegate {
|
||||
) -> Self {
|
||||
Self {
|
||||
command_palette,
|
||||
all_commands: commands.clone(),
|
||||
matches: vec![],
|
||||
commands,
|
||||
selected_ix: 0,
|
||||
@@ -164,7 +169,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
query: String,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let mut commands = self.commands.clone();
|
||||
let mut commands = self.all_commands.clone();
|
||||
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
cx.read_global::<HitCounts, _>(|hit_counts, _| {
|
||||
@@ -224,6 +229,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
@@ -239,7 +245,6 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
commands.push(Command {
|
||||
name: string.clone(),
|
||||
action,
|
||||
keystrokes: vec![],
|
||||
});
|
||||
matches.insert(
|
||||
0,
|
||||
@@ -251,6 +256,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
@@ -269,7 +275,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -280,12 +286,16 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
}
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
let command = self.commands.swap_remove(action_ix);
|
||||
self.matches.clear();
|
||||
self.commands.clear();
|
||||
cx.update_global(|hit_counts: &mut HitCounts, _| {
|
||||
*hit_counts.0.entry(command.name).or_default() += 1;
|
||||
});
|
||||
let action = command.action;
|
||||
cx.focus(&self.previous_focus_handle);
|
||||
cx.dispatch_action(action);
|
||||
cx.window_context()
|
||||
.spawn(move |mut cx| async move { cx.update(|_, cx| cx.dispatch_action(action)) })
|
||||
.detach_and_log_err(cx);
|
||||
self.dismissed(cx);
|
||||
}
|
||||
|
||||
@@ -295,13 +305,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let Some(r#match) = self.matches.get(ix) else {
|
||||
return None;
|
||||
};
|
||||
let Some(command) = self.commands.get(r#match.candidate_id) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let r#match = self.matches.get(ix)?;
|
||||
let command = self.commands.get(r#match.candidate_id)?;
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).selected(selected).child(
|
||||
h_stack()
|
||||
@@ -311,7 +316,11 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
command.name.clone(),
|
||||
r#match.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action(&*command.action, cx)),
|
||||
.children(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
&self.previous_focus_handle,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -345,8 +354,7 @@ impl std::fmt::Debug for Command {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Command")
|
||||
.field("name", &self.name)
|
||||
.field("keystrokes", &self.keystrokes)
|
||||
.finish()
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,7 +364,9 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use go_to_line::GoToLine;
|
||||
use gpui::TestAppContext;
|
||||
use language::Point;
|
||||
use project::Project;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
@@ -379,7 +389,6 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_command_palette(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
@@ -429,7 +438,7 @@ mod tests {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("editor");
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
@@ -449,12 +458,46 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_go_to_line(cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
cx.simulate_keystrokes("cmd-n");
|
||||
|
||||
let editor = workspace.update(cx, |workspace, cx| {
|
||||
workspace.active_item_as::<Editor>(cx).unwrap()
|
||||
});
|
||||
editor.update(cx, |editor, cx| editor.set_text("1\n2\n3\n4\n5\n6\n", cx));
|
||||
|
||||
cx.simulate_keystrokes("cmd-shift-p");
|
||||
cx.simulate_input("go to line: Toggle");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
assert!(workspace.active_modal::<GoToLine>(cx).is_some())
|
||||
});
|
||||
|
||||
cx.simulate_keystrokes("3 enter");
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert!(editor.focus_handle(cx).is_focused(cx));
|
||||
assert_eq!(
|
||||
editor.selections.last::<Point>(cx).range().start,
|
||||
Point::new(2, 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
menu::init();
|
||||
go_to_line::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(cx);
|
||||
Project::init_settings(cx);
|
||||
|
||||
@@ -28,7 +28,7 @@ theme = { path = "../theme" }
|
||||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -58,16 +58,16 @@ pub fn init(
|
||||
cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
|
||||
match status {
|
||||
Status::Disabled => {
|
||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
Status::Authorized => {
|
||||
filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
_ => {
|
||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1093,14 +1093,14 @@ mod tests {
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1119,10 +1119,10 @@ mod tests {
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_2_uri.clone(),
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Goodbye".into()
|
||||
"Hello world".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
@@ -1131,10 +1131,10 @@ mod tests {
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
buffer_2_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Hello world".into()
|
||||
"Goodbye".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -28,7 +28,8 @@ theme = { package = "theme2", path = "../theme2" }
|
||||
lsp = { package = "lsp2", path = "../lsp2" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
@@ -45,6 +46,6 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
|
||||
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
||||
@@ -22,6 +22,7 @@ use request::StatusNotification;
|
||||
use settings::SettingsStore;
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
ffi::OsString,
|
||||
mem,
|
||||
ops::Range,
|
||||
@@ -32,13 +33,17 @@ use util::{
|
||||
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
|
||||
};
|
||||
|
||||
// todo!()
|
||||
// const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
||||
actions!(SignIn, SignOut);
|
||||
|
||||
// todo!()
|
||||
// const COPILOT_NAMESPACE: &'static str = "copilot";
|
||||
actions!(Suggest, NextSuggestion, PreviousSuggestion, Reinstall);
|
||||
actions!(
|
||||
copilot,
|
||||
[
|
||||
Suggest,
|
||||
NextSuggestion,
|
||||
PreviousSuggestion,
|
||||
Reinstall,
|
||||
SignIn,
|
||||
SignOut
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(
|
||||
new_server_id: LanguageServerId,
|
||||
@@ -51,52 +56,70 @@ pub fn init(
|
||||
move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
|
||||
});
|
||||
cx.set_global(copilot.clone());
|
||||
cx.observe(&copilot, |handle, cx| {
|
||||
let copilot_action_types = [
|
||||
TypeId::of::<Suggest>(),
|
||||
TypeId::of::<NextSuggestion>(),
|
||||
TypeId::of::<PreviousSuggestion>(),
|
||||
TypeId::of::<Reinstall>(),
|
||||
];
|
||||
let copilot_auth_action_types = [TypeId::of::<SignOut>()];
|
||||
let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
|
||||
let status = handle.read(cx).status();
|
||||
let filter = cx.default_global::<collections::CommandPaletteFilter>();
|
||||
|
||||
// TODO
|
||||
// cx.observe(&copilot, |handle, cx| {
|
||||
// let status = handle.read(cx).status();
|
||||
// cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
|
||||
// match status {
|
||||
// Status::Disabled => {
|
||||
// filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// Status::Authorized => {
|
||||
// filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// _ => {
|
||||
// filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// })
|
||||
// .detach();
|
||||
match status {
|
||||
Status::Disabled => {
|
||||
filter.hidden_action_types.extend(copilot_action_types);
|
||||
filter.hidden_action_types.extend(copilot_auth_action_types);
|
||||
filter
|
||||
.hidden_action_types
|
||||
.extend(copilot_no_auth_action_types);
|
||||
}
|
||||
Status::Authorized => {
|
||||
filter
|
||||
.hidden_action_types
|
||||
.extend(copilot_no_auth_action_types);
|
||||
for type_id in copilot_action_types
|
||||
.iter()
|
||||
.chain(&copilot_auth_action_types)
|
||||
{
|
||||
filter.hidden_action_types.remove(type_id);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
filter.hidden_action_types.extend(copilot_action_types);
|
||||
filter.hidden_action_types.extend(copilot_auth_action_types);
|
||||
for type_id in &copilot_no_auth_action_types {
|
||||
filter.hidden_action_types.remove(type_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// sign_in::init(cx);
|
||||
// cx.add_global_action(|_: &SignIn, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// });
|
||||
// cx.add_global_action(|_: &SignOut, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// });
|
||||
|
||||
// cx.add_global_action(|_: &Reinstall, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
// .detach();
|
||||
// }
|
||||
// });
|
||||
sign_in::init(cx);
|
||||
cx.on_action(|_: &SignIn, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.on_action(|_: &SignOut, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.on_action(|_: &Reinstall, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enum CopilotServer {
|
||||
@@ -1002,229 +1025,231 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use gpui::{executor::Deterministic, TestAppContext};
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
// #[gpui::test(iterations = 10)]
|
||||
// async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
// deterministic.forbid_parking();
|
||||
// let (copilot, mut lsp) = Copilot::fake(cx);
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
let (copilot, mut lsp) = Copilot::fake(cx);
|
||||
|
||||
// let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
|
||||
// let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
|
||||
// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Hello".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello"));
|
||||
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
|
||||
.parse()
|
||||
.unwrap();
|
||||
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Hello".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
|
||||
// let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
|
||||
// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_2_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Goodbye".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye"));
|
||||
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
|
||||
.parse()
|
||||
.unwrap();
|
||||
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_2_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Goodbye".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidChangeTextDocumentParams {
|
||||
// text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
|
||||
// content_changes: vec![lsp::TextDocumentContentChangeEvent {
|
||||
// range: Some(lsp::Range::new(
|
||||
// lsp::Position::new(0, 5),
|
||||
// lsp::Position::new(0, 5)
|
||||
// )),
|
||||
// range_length: None,
|
||||
// text: " world".into(),
|
||||
// }],
|
||||
// }
|
||||
// );
|
||||
buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
.await,
|
||||
lsp::DidChangeTextDocumentParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
|
||||
content_changes: vec![lsp::TextDocumentContentChangeEvent {
|
||||
range: Some(lsp::Range::new(
|
||||
lsp::Position::new(0, 5),
|
||||
lsp::Position::new(0, 5)
|
||||
)),
|
||||
range_length: None,
|
||||
text: " world".into(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure updates to the file are reflected in the LSP.
|
||||
// buffer_1
|
||||
// .update(cx, |buffer, cx| {
|
||||
// buffer.file_updated(
|
||||
// Arc::new(File {
|
||||
// abs_path: "/root/child/buffer-1".into(),
|
||||
// path: Path::new("child/buffer-1").into(),
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .await;
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||
// }
|
||||
// );
|
||||
// let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 1,
|
||||
// "Hello world".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// Ensure updates to the file are reflected in the LSP.
|
||||
buffer_1.update(cx, |buffer, cx| {
|
||||
buffer.file_updated(
|
||||
Arc::new(File {
|
||||
abs_path: "/root/child/buffer-1".into(),
|
||||
path: Path::new("child/buffer-1").into(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||
}
|
||||
);
|
||||
let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
1,
|
||||
"Hello world".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure all previously-registered buffers are closed when signing out.
|
||||
// lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
|
||||
// Ok(request::SignOutResult {})
|
||||
// });
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
// }
|
||||
// );
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
// }
|
||||
// );
|
||||
// Ensure all previously-registered buffers are closed when signing out.
|
||||
lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
|
||||
Ok(request::SignOutResult {})
|
||||
});
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
// todo!() po: these notifications now happen in reverse order?
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure all previously-registered buffers are re-opened when signing in.
|
||||
// lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
|
||||
// Ok(request::SignInInitiateResult::AlreadySignedIn {
|
||||
// user: "user-1".into(),
|
||||
// })
|
||||
// });
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_2_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Goodbye".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Hello world".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// Ensure all previously-registered buffers are re-opened when signing in.
|
||||
lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
|
||||
Ok(request::SignInInitiateResult::AlreadySignedIn {
|
||||
user: "user-1".into(),
|
||||
})
|
||||
});
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// // Dropping a buffer causes it to be closed on the LSP side as well.
|
||||
// cx.update(|_| drop(buffer_2));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Hello world".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_2_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Goodbye".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
// Dropping a buffer causes it to be closed on the LSP side as well.
|
||||
cx.update(|_| drop(buffer_2));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// struct File {
|
||||
// abs_path: PathBuf,
|
||||
// path: Arc<Path>,
|
||||
// }
|
||||
struct File {
|
||||
abs_path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
}
|
||||
|
||||
// impl language2::File for File {
|
||||
// fn as_local(&self) -> Option<&dyn language2::LocalFile> {
|
||||
// Some(self)
|
||||
// }
|
||||
impl language::File for File {
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
// fn mtime(&self) -> std::time::SystemTime {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn mtime(&self) -> std::time::SystemTime {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn path(&self) -> &Arc<Path> {
|
||||
// &self.path
|
||||
// }
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
// fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn is_deleted(&self) -> bool {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn as_any(&self) -> &dyn std::any::Any {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn to_proto(&self) -> rpc::proto::File {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn worktree_id(&self) -> usize {
|
||||
// 0
|
||||
// }
|
||||
// }
|
||||
fn worktree_id(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// impl language::LocalFile for File {
|
||||
// fn abs_path(&self, _: &AppContext) -> PathBuf {
|
||||
// self.abs_path.clone()
|
||||
// }
|
||||
impl language::LocalFile for File {
|
||||
fn abs_path(&self, _: &AppContext) -> PathBuf {
|
||||
self.abs_path.clone()
|
||||
}
|
||||
|
||||
// fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn buffer_reloaded(
|
||||
// &self,
|
||||
// _: u64,
|
||||
// _: &clock::Global,
|
||||
// _: language::RopeFingerprint,
|
||||
// _: language::LineEnding,
|
||||
// _: std::time::SystemTime,
|
||||
// _: &mut AppContext,
|
||||
// ) {
|
||||
// unimplemented!()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
fn buffer_reloaded(
|
||||
&self,
|
||||
_: u64,
|
||||
_: &clock::Global,
|
||||
_: language::RopeFingerprint,
|
||||
_: language::LineEnding,
|
||||
_: std::time::SystemTime,
|
||||
_: &mut AppContext,
|
||||
) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,376 +1,213 @@
|
||||
// TODO add logging in
|
||||
// use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
// use gpui::{
|
||||
// elements::*,
|
||||
// geometry::rect::RectF,
|
||||
// platform::{WindowBounds, WindowKind, WindowOptions},
|
||||
// AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
|
||||
// WindowHandle,
|
||||
// };
|
||||
// use theme::ui::modal;
|
||||
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use gpui::{
|
||||
div, size, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, InteractiveElement,
|
||||
IntoElement, ParentElement, Point, Render, Stateful, Styled, ViewContext, VisualContext,
|
||||
WindowBounds, WindowHandle, WindowKind, WindowOptions,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, Button, Icon, IconElement, Label};
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct CopyUserCode;
|
||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct OpenGithub;
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
|
||||
cx.observe(&copilot, move |copilot, cx| {
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
match &status {
|
||||
crate::Status::SigningIn { prompt } => {
|
||||
if let Some(window) = verification_window.as_mut() {
|
||||
let updated = window
|
||||
.update(cx, |verification, cx| {
|
||||
verification.set_status(status.clone(), cx);
|
||||
cx.activate_window();
|
||||
})
|
||||
.is_ok();
|
||||
if !updated {
|
||||
verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
} else if let Some(_prompt) = prompt {
|
||||
verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
}
|
||||
Status::Authorized | Status::Unauthorized => {
|
||||
if let Some(window) = verification_window.as_ref() {
|
||||
window
|
||||
.update(cx, |verification, cx| {
|
||||
verification.set_status(status, cx);
|
||||
cx.activate(true);
|
||||
cx.activate_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(code_verification) = verification_window.take() {
|
||||
code_verification
|
||||
.update(cx, |_, cx| cx.remove_window())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
|
||||
// cx.observe(&copilot, move |copilot, cx| {
|
||||
// let status = copilot.read(cx).status();
|
||||
fn create_copilot_auth_window(
|
||||
cx: &mut AppContext,
|
||||
status: &Status,
|
||||
) -> WindowHandle<CopilotCodeVerification> {
|
||||
let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.));
|
||||
let window_options = WindowOptions {
|
||||
bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)),
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: true,
|
||||
display_id: None,
|
||||
};
|
||||
let window = cx.open_window(window_options, |cx| {
|
||||
cx.build_view(|_| CopilotCodeVerification::new(status.clone()))
|
||||
});
|
||||
window
|
||||
}
|
||||
|
||||
// match &status {
|
||||
// crate::Status::SigningIn { prompt } => {
|
||||
// if let Some(window) = verification_window.as_mut() {
|
||||
// let updated = window
|
||||
// .root(cx)
|
||||
// .map(|root| {
|
||||
// root.update(cx, |verification, cx| {
|
||||
// verification.set_status(status.clone(), cx);
|
||||
// cx.activate_window();
|
||||
// })
|
||||
// })
|
||||
// .is_some();
|
||||
// if !updated {
|
||||
// verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
// }
|
||||
// } else if let Some(_prompt) = prompt {
|
||||
// verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
// }
|
||||
// }
|
||||
// Status::Authorized | Status::Unauthorized => {
|
||||
// if let Some(window) = verification_window.as_ref() {
|
||||
// if let Some(verification) = window.root(cx) {
|
||||
// verification.update(cx, |verification, cx| {
|
||||
// verification.set_status(status, cx);
|
||||
// cx.platform().activate(true);
|
||||
// cx.activate_window();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {
|
||||
// if let Some(code_verification) = verification_window.take() {
|
||||
// code_verification.update(cx, |cx| cx.remove_window());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// }
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
}
|
||||
|
||||
// fn create_copilot_auth_window(
|
||||
// cx: &mut AppContext,
|
||||
// status: &Status,
|
||||
// ) -> WindowHandle<CopilotCodeVerification> {
|
||||
// let window_size = theme::current(cx).copilot.modal.dimensions();
|
||||
// let window_options = WindowOptions {
|
||||
// bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
|
||||
// titlebar: None,
|
||||
// center: true,
|
||||
// focus: true,
|
||||
// show: true,
|
||||
// kind: WindowKind::Normal,
|
||||
// is_movable: true,
|
||||
// screen: None,
|
||||
// };
|
||||
// cx.add_window(window_options, |_cx| {
|
||||
// CopilotCodeVerification::new(status.clone())
|
||||
// })
|
||||
// }
|
||||
impl CopilotCodeVerification {
|
||||
pub fn new(status: Status) -> Self {
|
||||
Self {
|
||||
status,
|
||||
connect_clicked: false,
|
||||
}
|
||||
}
|
||||
|
||||
// pub struct CopilotCodeVerification {
|
||||
// status: Status,
|
||||
// connect_clicked: bool,
|
||||
// }
|
||||
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
self.status = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// impl CopilotCodeVerification {
|
||||
// pub fn new(status: Status) -> Self {
|
||||
// Self {
|
||||
// status,
|
||||
// connect_clicked: false,
|
||||
// }
|
||||
// }
|
||||
fn render_device_code(
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let copied = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text() == &data.user_code)
|
||||
.unwrap_or(false);
|
||||
h_stack()
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
let user_code = data.user_code.clone();
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.child(Label::new(data.user_code.clone()))
|
||||
.child(div())
|
||||
.child(Label::new(if copied { "Copied!" } else { "Copy" }))
|
||||
}
|
||||
|
||||
// pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
// self.status = status;
|
||||
// cx.notify();
|
||||
// }
|
||||
fn render_prompting_modal(
|
||||
connect_clicked: bool,
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element {
|
||||
let connect_button_label = if connect_clicked {
|
||||
"Waiting for connection..."
|
||||
} else {
|
||||
"Connect to Github"
|
||||
};
|
||||
v_stack()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(Label::new(
|
||||
"Enable Copilot by connecting your existing license",
|
||||
))
|
||||
.child(Self::render_device_code(data, cx))
|
||||
.child(
|
||||
Label::new("Paste this code into GitHub after clicking the button below.")
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Button::new("connect-button", connect_button_label).on_click({
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
cx.listener(move |this, _, cx| {
|
||||
cx.open_url(&verification_uri);
|
||||
this.connect_clicked = true;
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
fn render_enabled_modal() -> impl Element {
|
||||
v_stack()
|
||||
.child(Label::new("Copilot Enabled!"))
|
||||
.child(Label::new(
|
||||
"You can update your settings or sign out from the Copilot menu in the status bar.",
|
||||
))
|
||||
.child(
|
||||
Button::new("copilot-enabled-done-button", "Done")
|
||||
.on_click(|_, cx| cx.remove_window()),
|
||||
)
|
||||
}
|
||||
|
||||
// fn render_device_code(
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> impl IntoAnyElement<Self> {
|
||||
// let copied = cx
|
||||
// .read_from_clipboard()
|
||||
// .map(|item| item.text() == &data.user_code)
|
||||
// .unwrap_or(false);
|
||||
fn render_unauthorized_modal() -> impl Element {
|
||||
v_stack()
|
||||
.child(Label::new(
|
||||
"Enable Copilot by connecting your existing license.",
|
||||
))
|
||||
.child(
|
||||
Label::new("You must have an active Copilot license to use it in Zed.")
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| {
|
||||
cx.remove_window();
|
||||
cx.open_url(COPILOT_SIGN_UP_URL)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// let device_code_style = &style.auth.prompting.device_code;
|
||||
impl Render for CopilotCodeVerification {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
// MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Label::new(data.user_code.clone(), device_code_style.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(device_code_style.left_container)
|
||||
// .constrained()
|
||||
// .with_width(device_code_style.left),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// if copied { "Copied!" } else { "Copy" },
|
||||
// device_code_style.cta.style_for(state).text.clone(),
|
||||
// )
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(*device_code_style.right_container.style_for(state))
|
||||
// .constrained()
|
||||
// .with_width(device_code_style.right),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(device_code_style.cta.style_for(state).container)
|
||||
// })
|
||||
// .on_click(gpui::platform::MouseButton::Left, {
|
||||
// let user_code = data.user_code.clone();
|
||||
// move |_, _, cx| {
|
||||
// cx.platform()
|
||||
// .write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
// cx.notify();
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
|
||||
// }
|
||||
|
||||
// fn render_prompting_modal(
|
||||
// connect_clicked: bool,
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum ConnectButton {}
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Enable Copilot by connecting",
|
||||
// style.auth.prompting.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "your existing license.",
|
||||
// style.auth.prompting.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.auth.prompting.subheading.container),
|
||||
// )
|
||||
// .with_child(Self::render_device_code(data, &style, cx))
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Paste this code into GitHub after",
|
||||
// style.auth.prompting.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "clicking the button below.",
|
||||
// style.auth.prompting.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.auth.prompting.hint.container.clone()),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
|
||||
// if connect_clicked {
|
||||
// "Waiting for connection..."
|
||||
// } else {
|
||||
// "Connect to GitHub"
|
||||
// },
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// {
|
||||
// let verification_uri = data.verification_uri.clone();
|
||||
// move |_, verification, cx| {
|
||||
// cx.platform().open_url(&verification_uri);
|
||||
// verification.connect_clicked = true;
|
||||
// }
|
||||
// },
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_enabled_modal(
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum DoneButton {}
|
||||
|
||||
// let enabled_style = &style.auth.authorized;
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
|
||||
// .contained()
|
||||
// .with_style(enabled_style.subheading.container)
|
||||
// .aligned(),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "You can update your settings or",
|
||||
// enabled_style.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "sign out from the Copilot menu in",
|
||||
// enabled_style.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(enabled_style.hint.container),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
|
||||
// "Done",
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// |_, _, cx| cx.remove_window(),
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_unauthorized_modal(
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// let unauthorized_style = &style.auth.not_authorized;
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Enable Copilot by connecting",
|
||||
// unauthorized_style.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "your existing license.",
|
||||
// unauthorized_style.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(unauthorized_style.subheading.container),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "You must have an active copilot",
|
||||
// unauthorized_style.warning.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "license to use it in Zed.",
|
||||
// unauthorized_style.warning.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(unauthorized_style.warning.container),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<Self, _, _, _>(
|
||||
// "Subscribe on GitHub",
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// |_, _, cx| {
|
||||
// cx.remove_window();
|
||||
// cx.platform().open_url(COPILOT_SIGN_UP_URL)
|
||||
// },
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Entity for CopilotCodeVerification {
|
||||
// type Event = ();
|
||||
// }
|
||||
|
||||
// impl View for CopilotCodeVerification {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "CopilotCodeVerification"
|
||||
// }
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// cx.notify()
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// cx.notify()
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// enum ConnectModal {}
|
||||
|
||||
// let style = theme::current(cx).clone();
|
||||
|
||||
// modal::<ConnectModal, _, _, _, _>(
|
||||
// "Connect Copilot to Zed",
|
||||
// &style.copilot.modal,
|
||||
// cx,
|
||||
// |cx| {
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// theme::ui::icon(&style.copilot.auth.header).into_any(),
|
||||
// match &self.status {
|
||||
// Status::SigningIn {
|
||||
// prompt: Some(prompt),
|
||||
// } => Self::render_prompting_modal(
|
||||
// self.connect_clicked,
|
||||
// &prompt,
|
||||
// &style.copilot,
|
||||
// cx,
|
||||
// ),
|
||||
// Status::Unauthorized => {
|
||||
// self.connect_clicked = false;
|
||||
// Self::render_unauthorized_modal(&style.copilot, cx)
|
||||
// }
|
||||
// Status::Authorized => {
|
||||
// self.connect_clicked = false;
|
||||
// Self::render_enabled_modal(&style.copilot, cx)
|
||||
// }
|
||||
// _ => Empty::new().into_any(),
|
||||
// },
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// },
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let prompt = match &self.status {
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
|
||||
Status::Unauthorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_unauthorized_modal().into_any_element()
|
||||
}
|
||||
Status::Authorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_enabled_modal().into_any_element()
|
||||
}
|
||||
_ => div().into_any_element(),
|
||||
};
|
||||
div()
|
||||
.id("copilot code verification")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.p_10()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(ui::Label::new("Connect Copilot to Zed"))
|
||||
.child(IconElement::new(Icon::ZedXCopilot))
|
||||
.child(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
27
crates/copilot_button2/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "copilot_button2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/copilot_button.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
copilot = { package = "copilot2", path = "../copilot2" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fs = { package = "fs2", path = "../fs2" }
|
||||
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
anyhow.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
370
crates/copilot_button2/src/copilot_button.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
#![allow(unused)]
|
||||
use anyhow::Result;
|
||||
use copilot::{Copilot, SignOut, Status};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
div, Action, AnchorCorner, AppContext, AsyncAppContext, AsyncWindowContext, Div, Entity,
|
||||
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||
File, Language,
|
||||
};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
ui::{
|
||||
popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, PopoverMenu, Tooltip,
|
||||
},
|
||||
StatusItemView, Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
const COPILOT_STARTING_TOAST_ID: usize = 1337;
|
||||
const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||
|
||||
pub struct CopilotButton {
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl Render for CopilotButton {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
if !all_language_settings.copilot.feature_enabled {
|
||||
return div();
|
||||
}
|
||||
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return div();
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
let enabled = self
|
||||
.editor_enabled
|
||||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||
|
||||
let icon = match status {
|
||||
Status::Error(_) => Icon::CopilotError,
|
||||
Status::Authorized => {
|
||||
if enabled {
|
||||
Icon::Copilot
|
||||
} else {
|
||||
Icon::CopilotDisabled
|
||||
}
|
||||
}
|
||||
_ => Icon::CopilotInit,
|
||||
};
|
||||
|
||||
if let Status::Error(e) = status {
|
||||
return div().child(
|
||||
IconButton::new("copilot-error", icon)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
COPILOT_ERROR_TOAST_ID,
|
||||
format!("Copilot can't be started: {}", e),
|
||||
)
|
||||
.on_click(
|
||||
"Reinstall Copilot",
|
||||
|cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
);
|
||||
}
|
||||
let this = cx.view().clone();
|
||||
|
||||
div().child(
|
||||
popover_menu("copilot")
|
||||
.menu(move |cx| match status {
|
||||
Status::Authorized => this.update(cx, |this, cx| this.build_copilot_menu(cx)),
|
||||
_ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
|
||||
})
|
||||
.anchor(AnchorCorner::BottomRight)
|
||||
.trigger(
|
||||
IconButton::new("copilot-icon", icon)
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
file: None,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
ContextMenu::build(cx, |menu, cx| {
|
||||
menu.entry("Sign In", initiate_sign_in)
|
||||
.entry("Disable Copilot", move |cx| hide_copilot(fs.clone(), cx))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
return ContextMenu::build(cx, move |mut menu, cx| {
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(&language), None, cx)
|
||||
.show_copilot_suggestions;
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language.name()
|
||||
),
|
||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||
);
|
||||
}
|
||||
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
if let Some(file) = &self.file {
|
||||
let path = file.path().clone();
|
||||
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for This Path",
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
move |cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
if let Ok(workspace) = workspace.root_view(cx) {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.spawn(|cx| {
|
||||
configure_disabled_globs(
|
||||
workspace,
|
||||
path_enabled.then_some(path.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let globally_enabled = settings.copilot_enabled(None, None);
|
||||
menu.entry(
|
||||
if globally_enabled {
|
||||
"Hide Suggestions for All Files"
|
||||
} else {
|
||||
"Show Suggestions for All Files"
|
||||
},
|
||||
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
||||
)
|
||||
.separator()
|
||||
.link(
|
||||
"Copilot Settings",
|
||||
OpenBrowser {
|
||||
url: COPILOT_SETTINGS_URL.to_string(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
.action("Sign Out", SignOut.boxed_clone())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
let language = snapshot.language_at(suggestion_anchor);
|
||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||
|
||||
self.editor_enabled = Some(
|
||||
all_language_settings(self.file.as_ref(), cx)
|
||||
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
||||
);
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CopilotButton {
|
||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
|
||||
self.editor_subscription = Some((
|
||||
cx.observe(&editor, Self::update_enabled),
|
||||
editor.entity_id().as_u64() as usize,
|
||||
));
|
||||
self.update_enabled(editor, cx);
|
||||
} else {
|
||||
self.language = None;
|
||||
self.editor_subscription = None;
|
||||
self.editor_enabled = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure_disabled_globs(
|
||||
workspace: WeakView<Workspace>,
|
||||
path_to_disable: Option<Arc<Path>>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
|
||||
let copilot = file.copilot.get_or_insert_with(Default::default);
|
||||
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
||||
settings
|
||||
.get::<AllLanguageSettings>(None)
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.iter()
|
||||
.map(|glob| glob.glob().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(path_to_disable) = &path_to_disable {
|
||||
globs.push(path_to_disable.to_string_lossy().into_owned());
|
||||
} else {
|
||||
globs.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if !edits.is_empty() {
|
||||
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
|
||||
});
|
||||
|
||||
// When *enabling* a path, don't actually perform an edit, just select the range.
|
||||
if path_to_disable.is_some() {
|
||||
item.edit(edits.iter().cloned(), cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions =
|
||||
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.languages
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||
});
|
||||
}
|
||||
|
||||
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.features.get_or_insert(Default::default()).copilot = Some(false);
|
||||
});
|
||||
}
|
||||
|
||||
fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ impl View for ProjectDiagnosticsEditor {
|
||||
json!({
|
||||
"project": json!({
|
||||
"language_servers": project.language_server_statuses().collect::<Vec<_>>(),
|
||||
"summary": project.diagnostic_summary(cx),
|
||||
"summary": project.diagnostic_summary(false, cx),
|
||||
}),
|
||||
"summary": self.summary,
|
||||
"paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)|
|
||||
@@ -195,7 +195,7 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
|
||||
let project = project_handle.read(cx);
|
||||
let summary = project.diagnostic_summary(cx);
|
||||
let summary = project.diagnostic_summary(false, cx);
|
||||
let mut this = Self {
|
||||
project: project_handle,
|
||||
summary,
|
||||
@@ -241,7 +241,7 @@ impl ProjectDiagnosticsEditor {
|
||||
let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
|
||||
.project
|
||||
.read(cx)
|
||||
.diagnostic_summaries(cx)
|
||||
.diagnostic_summaries(false, cx)
|
||||
.fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
|
||||
summaries.entry(server_id).or_default().insert(path);
|
||||
summaries
|
||||
@@ -320,7 +320,7 @@ impl ProjectDiagnosticsEditor {
|
||||
.context("rechecking diagnostics for paths")?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.summary = this.project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = this.project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(Event::TitleChanged);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
|
||||
@@ -34,19 +34,19 @@ impl DiagnosticIndicator {
|
||||
}
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(cx),
|
||||
summary: project.read(cx).diagnostic_summary(false, cx),
|
||||
in_progress_checks: project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
|
||||
@@ -13,10 +13,10 @@ use editor::{
|
||||
};
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
|
||||
FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement,
|
||||
Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||
VisualContext, WeakView, WindowContext,
|
||||
actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusHandle,
|
||||
Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render,
|
||||
SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowContext,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
|
||||
@@ -36,14 +36,14 @@ use std::{
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
|
||||
use ui::{h_stack, prelude::*, HighlightedLabel, Icon, IconElement, Label};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
actions!(Deploy, ToggleWarnings);
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
|
||||
const CONTEXT_LINE_COUNT: u32 = 1;
|
||||
|
||||
@@ -88,7 +88,7 @@ struct DiagnosticGroupState {
|
||||
block_count: usize,
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
type Element = Focusable<Div>;
|
||||
@@ -109,7 +109,6 @@ impl Render for ProjectDiagnosticsEditor {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.on_focus_in(cx.listener(Self::focus_in))
|
||||
.on_action(cx.listener(Self::toggle_warnings))
|
||||
.child(child)
|
||||
}
|
||||
@@ -149,6 +148,11 @@ impl ProjectDiagnosticsEditor {
|
||||
_ => {}
|
||||
});
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let focus_in_subscription =
|
||||
cx.on_focus_in(&focus_handle, |diagnostics, cx| diagnostics.focus_in(cx));
|
||||
|
||||
let excerpts = cx.build_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id()));
|
||||
let editor = cx.build_view(|cx| {
|
||||
let mut editor =
|
||||
@@ -158,65 +162,35 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
let editor_event_subscription =
|
||||
cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
|
||||
Self::emit_item_event_for_editor_event(event, cx);
|
||||
cx.emit(event.clone());
|
||||
if event == &EditorEvent::Focused && this.path_states.is_empty() {
|
||||
cx.focus(&this.focus_handle);
|
||||
}
|
||||
});
|
||||
|
||||
let project = project_handle.read(cx);
|
||||
let summary = project.diagnostic_summary(cx);
|
||||
let summary = project.diagnostic_summary(false, cx);
|
||||
let mut this = Self {
|
||||
project: project_handle,
|
||||
summary,
|
||||
workspace,
|
||||
excerpts,
|
||||
focus_handle: cx.focus_handle(),
|
||||
focus_handle,
|
||||
editor,
|
||||
path_states: Default::default(),
|
||||
paths_to_update: HashMap::default(),
|
||||
include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
|
||||
current_diagnostics: HashMap::default(),
|
||||
_subscriptions: vec![project_event_subscription, editor_event_subscription],
|
||||
_subscriptions: vec![
|
||||
project_event_subscription,
|
||||
editor_event_subscription,
|
||||
focus_in_subscription,
|
||||
],
|
||||
};
|
||||
this.update_excerpts(None, cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
|
||||
|
||||
EditorEvent::Saved | EditorEvent::TitleChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::Reparsed => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::SelectionsChanged { local } if *local => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::DirtyChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
EditorEvent::BufferEdited => {
|
||||
cx.emit(ItemEvent::Edit);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
|
||||
cx.emit(ItemEvent::Edit);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
@@ -236,7 +210,7 @@ impl ProjectDiagnosticsEditor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: &FocusEvent, cx: &mut ViewContext<Self>) {
|
||||
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
|
||||
self.editor.focus_handle(cx).focus(cx)
|
||||
}
|
||||
@@ -252,7 +226,7 @@ impl ProjectDiagnosticsEditor {
|
||||
let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
|
||||
.project
|
||||
.read(cx)
|
||||
.diagnostic_summaries(cx)
|
||||
.diagnostic_summaries(false, cx)
|
||||
.fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
|
||||
summaries.entry(server_id).or_default().insert(path);
|
||||
summaries
|
||||
@@ -332,9 +306,8 @@ impl ProjectDiagnosticsEditor {
|
||||
.context("rechecking diagnostics for paths")?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.summary = this.project.read(cx).diagnostic_summary(cx);
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
this.summary = this.project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@@ -649,6 +622,12 @@ impl FocusableView for ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
impl Item for ProjectDiagnosticsEditor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
@@ -662,8 +641,44 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
Some("Project Diagnostics".into())
|
||||
}
|
||||
|
||||
fn tab_content(&self, _detail: Option<usize>, _: &WindowContext) -> AnyElement {
|
||||
render_summary(&self.summary)
|
||||
fn tab_content(&self, _detail: Option<usize>, selected: bool, _: &WindowContext) -> AnyElement {
|
||||
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
|
||||
let label = Label::new("No problems");
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when(self.summary.error_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(self.summary.error_count.to_string()).color(
|
||||
if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when(self.summary.warning_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.child(
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
|
||||
)
|
||||
.child(Label::new(self.summary.warning_count.to_string()).color(
|
||||
if selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn for_each_project_item(
|
||||
@@ -774,43 +789,43 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
Arc::new(move |_| {
|
||||
h_stack()
|
||||
.id("diagnostic header")
|
||||
.gap_3()
|
||||
.bg(gpui::red())
|
||||
.map(|stack| {
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||
} else {
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||
};
|
||||
|
||||
stack.child(div().pl_8().child(icon))
|
||||
})
|
||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||
stack.child(Label::new(format!("{source}:")).color(Color::Accent))
|
||||
})
|
||||
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(Label::new(code.clone()))
|
||||
})
|
||||
.py_2()
|
||||
.pl_10()
|
||||
.pr_5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_3()
|
||||
.map(|stack| {
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||
} else {
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||
};
|
||||
stack.child(icon)
|
||||
})
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(Label::new(format!("({code})")).color(Color::Muted))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||
stack.child(Label::new(format!("{source}")).color(Color::Muted))
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
let label = Label::new("No problems");
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.bg(gpui::red())
|
||||
.child(IconElement::new(Icon::XCircle))
|
||||
.child(Label::new(summary.error_count.to_string()))
|
||||
.child(IconElement::new(Icon::ExclamationTriangle))
|
||||
.child(Label::new(summary.warning_count.to_string()))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
|
||||
lhs: &DiagnosticEntry<L>,
|
||||
rhs: &DiagnosticEntry<R>,
|
||||
|
||||
@@ -77,13 +77,13 @@ impl DiagnosticIndicator {
|
||||
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ impl DiagnosticIndicator {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(cx),
|
||||
summary: project.read(cx).diagnostic_summary(false, cx),
|
||||
in_progress_checks: project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{Icon, IconButton, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
|
||||