Compare commits
1007 Commits
v0.105.4
...
element-tr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3da2e691c | ||
|
|
317acbf1d7 | ||
|
|
b52db5c5e6 | ||
|
|
30280ab897 | ||
|
|
4d621f355d | ||
|
|
05cbceec24 | ||
|
|
192b3512fd | ||
|
|
3a326bfa7e | ||
|
|
dd55ccef34 | ||
|
|
1e13e273d2 | ||
|
|
438cf529bb | ||
|
|
ec0b2e5430 | ||
|
|
21d4546a86 | ||
|
|
7832120a4c | ||
|
|
efbf0c828d | ||
|
|
c9d214e8ef | ||
|
|
38a7b39070 | ||
|
|
239b0c2f71 | ||
|
|
cc445f7cef | ||
|
|
258fcaea94 | ||
|
|
fc927f7406 | ||
|
|
d1adce5890 | ||
|
|
4e6fb9034d | ||
|
|
96f2c4a9de | ||
|
|
a72434f67b | ||
|
|
c0e8ae5dfa | ||
|
|
0de4a93ec7 | ||
|
|
da8919002f | ||
|
|
5247f217fd | ||
|
|
56462ef793 | ||
|
|
a0b667a2ca | ||
|
|
e7c04d4aca | ||
|
|
72435af170 | ||
|
|
ce75be91e1 | ||
|
|
db6a3e1783 | ||
|
|
50bbdd5cab | ||
|
|
5d10dc7e58 | ||
|
|
7171818d24 | ||
|
|
48033463c8 | ||
|
|
6ffeb048b3 | ||
|
|
5423012368 | ||
|
|
f4135e6bcf | ||
|
|
909fbb9538 | ||
|
|
89f4718ea1 | ||
|
|
2e2825ae98 | ||
|
|
3740c9d852 | ||
|
|
7bb99c9b9c | ||
|
|
aa3fb28f81 | ||
|
|
b7d30fca2b | ||
|
|
e4fe9538d7 | ||
|
|
f3979a9f28 | ||
|
|
825c352b6a | ||
|
|
b0b7f27f3a | ||
|
|
c831c5749a | ||
|
|
1409fc0da3 | ||
|
|
901af8de3f | ||
|
|
d9a030157e | ||
|
|
a0996c1807 | ||
|
|
a3dcaf21cb | ||
|
|
8ad7ebf02f | ||
|
|
47aa387b91 | ||
|
|
6150df71b2 | ||
|
|
fd94f2a5b5 | ||
|
|
847a1cb068 | ||
|
|
c1f7c9bb87 | ||
|
|
ac181183cc | ||
|
|
8a11053f1f | ||
|
|
b0acaed02f | ||
|
|
c3a917f8b3 | ||
|
|
766ee836b5 | ||
|
|
68bc22f9cd | ||
|
|
0609628645 | ||
|
|
32028fbbb1 | ||
|
|
e3d948f60b | ||
|
|
40c6f738b4 | ||
|
|
296fc92721 | ||
|
|
21b4ae3fdc | ||
|
|
d69105bb77 | ||
|
|
4a6c8ff809 | ||
|
|
1f6d9369d6 | ||
|
|
3a70f02cbf | ||
|
|
dd7e1c505c | ||
|
|
94f0140f62 | ||
|
|
28b29d0985 | ||
|
|
52f2521f6a | ||
|
|
e1e8b63eb5 | ||
|
|
a1aba32209 | ||
|
|
fa3916d1bf | ||
|
|
3ac7ef90ef | ||
|
|
4050bf43c4 | ||
|
|
0fbf84e6bc | ||
|
|
d91b423a45 | ||
|
|
2bbce2f0fd | ||
|
|
1be1bffb29 | ||
|
|
92542e6b94 | ||
|
|
3932c1064e | ||
|
|
d446b91117 | ||
|
|
673257bbbc | ||
|
|
180ed7da81 | ||
|
|
38d8ab2285 | ||
|
|
c17a4d8453 | ||
|
|
e74285f6d2 | ||
|
|
2189983323 | ||
|
|
58650b7d2d | ||
|
|
d5fc831321 | ||
|
|
743949753a | ||
|
|
184f5f2397 | ||
|
|
597aa0475e | ||
|
|
70984faee2 | ||
|
|
e657e4d1d1 | ||
|
|
9e20ccc01a | ||
|
|
1343ea66c9 | ||
|
|
2b90b8d6b7 | ||
|
|
90d34c1251 | ||
|
|
93ff79febf | ||
|
|
7fef03a7db | ||
|
|
30269381e8 | ||
|
|
9985f388ac | ||
|
|
a869de3b1f | ||
|
|
4aac733238 | ||
|
|
7ed891e0c6 | ||
|
|
a1f7a97ff5 | ||
|
|
bca97f7186 | ||
|
|
61e09ff532 | ||
|
|
8e465b4393 | ||
|
|
b16d37953d | ||
|
|
f5c76d93bc | ||
|
|
98c0e00a2c | ||
|
|
3d8e9a593e | ||
|
|
ffa3362e16 | ||
|
|
e34a488b55 | ||
|
|
c22778bd92 | ||
|
|
65828c14fc | ||
|
|
7cb00aeb34 | ||
|
|
7b2782c0f6 | ||
|
|
3f076eeda6 | ||
|
|
a35d350cbd | ||
|
|
a6a50113da | ||
|
|
8b637e194e | ||
|
|
289255d67a | ||
|
|
549e78d7b3 | ||
|
|
5a42ca6772 | ||
|
|
8dad3ad8ea | ||
|
|
159d798c34 | ||
|
|
856d23626f | ||
|
|
8890636a56 | ||
|
|
a0634fa79e | ||
|
|
03937a9f89 | ||
|
|
24086191af | ||
|
|
f0b9e9a89d | ||
|
|
6e3393c93f | ||
|
|
aa41f97e38 | ||
|
|
2b53c67789 | ||
|
|
731ce1721a | ||
|
|
8321b9430e | ||
|
|
7f69350e4d | ||
|
|
1a156c1060 | ||
|
|
7149f99f02 | ||
|
|
5491398a64 | ||
|
|
0e4bd485e0 | ||
|
|
fecb27232e | ||
|
|
f58a9bad42 | ||
|
|
f4d50c4dca | ||
|
|
036e266bae | ||
|
|
d98c347902 | ||
|
|
1270bcc6ed | ||
|
|
8914b94577 | ||
|
|
acca8ea786 | ||
|
|
eaef1c8b8e | ||
|
|
0dfe70125b | ||
|
|
5afd83c883 | ||
|
|
a61b34cab5 | ||
|
|
ad1b96720a | ||
|
|
296a2b8e5d | ||
|
|
597a9f9548 | ||
|
|
e031718747 | ||
|
|
6452ff203e | ||
|
|
79e0509bf9 | ||
|
|
218922d9f8 | ||
|
|
7a2b04a5d1 | ||
|
|
dc32e56a9c | ||
|
|
490cc7ded6 | ||
|
|
4db0350f06 | ||
|
|
edc52e5b28 | ||
|
|
a1a1284696 | ||
|
|
6f849e8f64 | ||
|
|
3e32504526 | ||
|
|
6e84d3cce0 | ||
|
|
8c02de6c61 | ||
|
|
f09df31480 | ||
|
|
a8697df9e3 | ||
|
|
6f30d6b4d0 | ||
|
|
61490fbaa8 | ||
|
|
4ce7f059c3 | ||
|
|
deb0e57c49 | ||
|
|
19c1a54fea | ||
|
|
850d43c1e8 | ||
|
|
ec368c8102 | ||
|
|
c04171abf6 | ||
|
|
488d08b43c | ||
|
|
18abb068b1 | ||
|
|
cec5280013 | ||
|
|
c126ff10a7 | ||
|
|
bb348c1353 | ||
|
|
fb1e7eef6b | ||
|
|
ac5b32c491 | ||
|
|
b526fc070d | ||
|
|
88ae4679d1 | ||
|
|
9e7a579365 | ||
|
|
b040ae8d4d | ||
|
|
c6e20aed9b | ||
|
|
695a24d8a7 | ||
|
|
2472142532 | ||
|
|
bf49f55c95 | ||
|
|
0df1eb71cb | ||
|
|
c8b452d411 | ||
|
|
708034d1d3 | ||
|
|
3127c78bc7 | ||
|
|
938dd8b9ca | ||
|
|
847376cd8f | ||
|
|
1a3650ef2a | ||
|
|
129273036a | ||
|
|
97d77440e7 | ||
|
|
5e43c332f1 | ||
|
|
6891e86621 | ||
|
|
3c1ec2e9ca | ||
|
|
49caeeafce | ||
|
|
349ad7858b | ||
|
|
c70f220db3 | ||
|
|
603765732e | ||
|
|
297b6b282c | ||
|
|
fedb787b4f | ||
|
|
90f226193c | ||
|
|
e477fa7a93 | ||
|
|
f3679b37a2 | ||
|
|
b30b1d145c | ||
|
|
e902d5d917 | ||
|
|
8bd4107423 | ||
|
|
7ba305e033 | ||
|
|
caa0eb6e29 | ||
|
|
c6d831a564 | ||
|
|
943c02bf79 | ||
|
|
c32b081029 | ||
|
|
44a30e269e | ||
|
|
ef18aaa66f | ||
|
|
45f3a98359 | ||
|
|
36bca4f0d6 | ||
|
|
6e5ad75c5c | ||
|
|
79a61c28d7 | ||
|
|
bac43ae38e | ||
|
|
e900ea20b7 | ||
|
|
8496d02fe1 | ||
|
|
fc94c4ea40 | ||
|
|
c90d976d7a | ||
|
|
d320d3a8bf | ||
|
|
24bab48043 | ||
|
|
f5d6d7caca | ||
|
|
85fe11ff11 | ||
|
|
30979caf25 | ||
|
|
ce8533f83b | ||
|
|
2e5461ee4d | ||
|
|
2044ccdc0b | ||
|
|
ca35573ad5 | ||
|
|
6dbe983461 | ||
|
|
262f5886a4 | ||
|
|
207d843aee | ||
|
|
a6b872bb0c | ||
|
|
8cd112110e | ||
|
|
9581279919 | ||
|
|
002458f4c8 | ||
|
|
a50977e0fd | ||
|
|
ef73bf799c | ||
|
|
7aea95704e | ||
|
|
564a8bdc19 | ||
|
|
09ef3ccf67 | ||
|
|
1f84cdb88c | ||
|
|
12d7d8db0a | ||
|
|
1bfde4bfa2 | ||
|
|
80c0a6ead3 | ||
|
|
7f4ebf50d3 | ||
|
|
a528c6c686 | ||
|
|
23f11fcd5e | ||
|
|
3dad0d9811 | ||
|
|
d920f7edc1 | ||
|
|
f37b83a0ea | ||
|
|
12573ed2e7 | ||
|
|
be1800884e | ||
|
|
93c233b1cf | ||
|
|
47b64a5074 | ||
|
|
d6fa06b3be | ||
|
|
e2da2b232e | ||
|
|
bdf1731db3 | ||
|
|
5477b87774 | ||
|
|
7478e63ea0 | ||
|
|
922d1462a8 | ||
|
|
8f410d5e2e | ||
|
|
0d8c743dfe | ||
|
|
e50f4c0ee5 | ||
|
|
b6a9c58994 | ||
|
|
382693a199 | ||
|
|
acf2c2c6a5 | ||
|
|
006f840570 | ||
|
|
2d6725a41a | ||
|
|
457df8d3f3 | ||
|
|
b6e4208ea8 | ||
|
|
56fba5541a | ||
|
|
4a88a9e253 | ||
|
|
a69dbafe3c | ||
|
|
a9c69bf774 | ||
|
|
986a516bf1 | ||
|
|
9bf22c56cd | ||
|
|
b5705e079f | ||
|
|
2ec2036c2f | ||
|
|
faf1d38a6d | ||
|
|
6c1c7eaf75 | ||
|
|
2d5741aef8 | ||
|
|
a9f80a603c | ||
|
|
658b58378e | ||
|
|
8a807102a6 | ||
|
|
afee29ad3f | ||
|
|
6ec3927dd3 | ||
|
|
b109075bf2 | ||
|
|
f4667cbc33 | ||
|
|
d021842fa1 | ||
|
|
f42cb109a0 | ||
|
|
1b70e7d0df | ||
|
|
b687270207 | ||
|
|
06cac18d78 | ||
|
|
6cac58b34c | ||
|
|
4b15a2bd63 | ||
|
|
e8409a0108 | ||
|
|
39ad3a625c | ||
|
|
2a5b9b635b | ||
|
|
e2056756ef | ||
|
|
6a8e3fd02d | ||
|
|
2a68f01402 | ||
|
|
dca93fb177 | ||
|
|
010bb73ac2 | ||
|
|
bb2cc2d157 | ||
|
|
86618a64c6 | ||
|
|
1ff17bd15d | ||
|
|
12ea12e4e7 | ||
|
|
4f956d71e2 | ||
|
|
ce6b31d938 | ||
|
|
a8387b8b19 | ||
|
|
a420d9cdc7 | ||
|
|
a8dfa01362 | ||
|
|
92f23e626e | ||
|
|
553abd01be | ||
|
|
eced842dfc | ||
|
|
b6a3d9ce59 | ||
|
|
eebbc807e5 | ||
|
|
0fb7364235 | ||
|
|
76191fe47d | ||
|
|
f1cc62c21f | ||
|
|
f53b63eaf6 | ||
|
|
821997d372 | ||
|
|
85b76b1143 | ||
|
|
9004254fbf | ||
|
|
1de9add304 | ||
|
|
7a39455af9 | ||
|
|
96d60eff23 | ||
|
|
a69f93d214 | ||
|
|
8e1638b773 | ||
|
|
95ef61bc45 | ||
|
|
c142676b20 | ||
|
|
be843227a1 | ||
|
|
48d9b49ada | ||
|
|
19f774a4a4 | ||
|
|
d7d027bcf1 | ||
|
|
e6228ca682 | ||
|
|
f2ee61553f | ||
|
|
30088afa89 | ||
|
|
40430cf01b | ||
|
|
b1d88ced61 | ||
|
|
5b7ca6435c | ||
|
|
a6ae6b0752 | ||
|
|
61b8ad38bd | ||
|
|
e714653478 | ||
|
|
d70b4f04f6 | ||
|
|
9eff99de49 | ||
|
|
4855b8f3de | ||
|
|
84ad2cb827 | ||
|
|
0e537cced4 | ||
|
|
b366592878 | ||
|
|
5cf92980f0 | ||
|
|
8f7f38536d | ||
|
|
97edec6e72 | ||
|
|
40d58c9bc3 | ||
|
|
f76c9041bb | ||
|
|
66af1707a1 | ||
|
|
96fbf9fd06 | ||
|
|
48a12be538 | ||
|
|
012a7743ad | ||
|
|
678235023f | ||
|
|
a4afb72535 | ||
|
|
1db24e5f2a | ||
|
|
639ae671ae | ||
|
|
1a4e9ecfef | ||
|
|
dcdd74dff4 | ||
|
|
fe60f264c4 | ||
|
|
dfdb691f73 | ||
|
|
9fe5836240 | ||
|
|
8074e6b46a | ||
|
|
d4ef764305 | ||
|
|
f763ed9a7e | ||
|
|
8922437fcd | ||
|
|
6e98cd5aad | ||
|
|
08f4576aa6 | ||
|
|
1d29709c32 | ||
|
|
7610028a89 | ||
|
|
bdcbf9b92e | ||
|
|
b807b3c785 | ||
|
|
90b54a45e8 | ||
|
|
bb85d6f63e | ||
|
|
0d903f4d0d | ||
|
|
ba4f4e0a3e | ||
|
|
312f3d2ab9 | ||
|
|
6b710dc146 | ||
|
|
def67295e5 | ||
|
|
0823a18cff | ||
|
|
ca735ad70f | ||
|
|
af90077a6a | ||
|
|
9cba45910e | ||
|
|
613973d2b1 | ||
|
|
29ccdb3cd9 | ||
|
|
1e4f5145cf | ||
|
|
a0ab9fe56b | ||
|
|
fb57299a1d | ||
|
|
162cb19cff | ||
|
|
abfb4490d5 | ||
|
|
7b610f8dd8 | ||
|
|
8b3a357949 | ||
|
|
f73708d725 | ||
|
|
d3c79c7078 | ||
|
|
d889cdecde | ||
|
|
2654942b3c | ||
|
|
ed2c8cdc25 | ||
|
|
19434afe0a | ||
|
|
a7c4ae530d | ||
|
|
b2d735e573 | ||
|
|
044701e907 | ||
|
|
42e9800bde | ||
|
|
d956bd3743 | ||
|
|
6084486dcd | ||
|
|
100a4731e2 | ||
|
|
000ae27aff | ||
|
|
06b0707aa9 | ||
|
|
ac93449788 | ||
|
|
02d32de044 | ||
|
|
8f4d81903c | ||
|
|
333e3e4f01 | ||
|
|
f7721d0523 | ||
|
|
e5473fc51a | ||
|
|
a08ceadd1a | ||
|
|
dc2ddfb42c | ||
|
|
4eeed14d34 | ||
|
|
5dbda70235 | ||
|
|
38d53a6fe2 | ||
|
|
6a4c2a0d40 | ||
|
|
77a932fe3b | ||
|
|
4b2c24dd8c | ||
|
|
8814ea8241 | ||
|
|
8f6649e29e | ||
|
|
73360d37f7 | ||
|
|
eb642551ac | ||
|
|
f33d41af63 | ||
|
|
5e7954f152 | ||
|
|
9e79ad5a62 | ||
|
|
0dcbc47e15 | ||
|
|
b8b8fe6120 | ||
|
|
ff066ef177 | ||
|
|
63e834ce73 | ||
|
|
b118e60160 | ||
|
|
00e8531898 | ||
|
|
7c8d662315 | ||
|
|
2f6d67cad6 | ||
|
|
f5e5b44bc1 | ||
|
|
f795177ab6 | ||
|
|
a4bde421db | ||
|
|
f6a4151f60 | ||
|
|
34b7537948 | ||
|
|
66120fb97a | ||
|
|
6de69de868 | ||
|
|
82577b4acc | ||
|
|
f6bc229d1d | ||
|
|
8db7f7ed37 | ||
|
|
d5ffd4a1fb | ||
|
|
b53579858a | ||
|
|
28d504d7d3 | ||
|
|
63a230f92e | ||
|
|
56c2ac048d | ||
|
|
208d5df106 | ||
|
|
d09f53c380 | ||
|
|
f8ca86c6a7 | ||
|
|
4128e2ffcb | ||
|
|
696aee3891 | ||
|
|
bcad2f4e9e | ||
|
|
1cf5cdbeca | ||
|
|
8e94f3902b | ||
|
|
3412bb75be | ||
|
|
17925ed563 | ||
|
|
43da36948b | ||
|
|
88a6a41c7c | ||
|
|
b58c42cd53 | ||
|
|
d37785c214 | ||
|
|
b369a6dc2a | ||
|
|
9f32a6e209 | ||
|
|
3f66caedfc | ||
|
|
1dd82df59e | ||
|
|
81bc86be07 | ||
|
|
663649a100 | ||
|
|
5ee6814947 | ||
|
|
65cd4f5838 | ||
|
|
7fd35d68bb | ||
|
|
ad8187b151 | ||
|
|
1e557dddcc | ||
|
|
456baaa112 | ||
|
|
2c7e37e9ff | ||
|
|
2d99b327fc | ||
|
|
79ad5c08e4 | ||
|
|
ca6eb5511c | ||
|
|
c46137e40d | ||
|
|
b391f5615b | ||
|
|
65c7765c07 | ||
|
|
e99f6c03c1 | ||
|
|
31062d424f | ||
|
|
559433bed0 | ||
|
|
8fafae2cfa | ||
|
|
b3c9473bc8 | ||
|
|
b77c815bcd | ||
|
|
13192fa03c | ||
|
|
b258ee5f77 | ||
|
|
a63eccf188 | ||
|
|
37de4a9990 | ||
|
|
c4870e1b6b | ||
|
|
6f7c305308 | ||
|
|
438dd42f7d | ||
|
|
f57d563578 | ||
|
|
c8535440d3 | ||
|
|
84ea34f918 | ||
|
|
44ada52185 | ||
|
|
78b1231386 | ||
|
|
fe3ef08f39 | ||
|
|
f1c743286d | ||
|
|
657a25178d | ||
|
|
f3560caf93 | ||
|
|
2e056e9b0b | ||
|
|
92bda1231e | ||
|
|
7643bd61fd | ||
|
|
bf73b40529 | ||
|
|
ed20397a2b | ||
|
|
1c70ca2214 | ||
|
|
77b9a7aa5a | ||
|
|
0d0c760d94 | ||
|
|
177e385bb9 | ||
|
|
699a5d2944 | ||
|
|
d298afba01 | ||
|
|
45d08c70f0 | ||
|
|
77feecc623 | ||
|
|
acffc7e7f0 | ||
|
|
b0e56b7c54 | ||
|
|
df2fa87e6b | ||
|
|
a27be35325 | ||
|
|
2f3c3d510f | ||
|
|
d09767a90b | ||
|
|
427a857e9a | ||
|
|
e9842091e4 | ||
|
|
332f3f5617 | ||
|
|
73e78a2257 | ||
|
|
f7cd0e84f9 | ||
|
|
a4e77af571 | ||
|
|
c8bc68c267 | ||
|
|
02d6b91b73 | ||
|
|
5074bccae4 | ||
|
|
7d94b0325f | ||
|
|
ff1722d307 | ||
|
|
e68b24f839 | ||
|
|
339ba7986f | ||
|
|
6cb674a0aa | ||
|
|
6db47478cf | ||
|
|
01b45f4f23 | ||
|
|
4d61d01943 | ||
|
|
dd0edcd203 | ||
|
|
ebc80597d5 | ||
|
|
d28c81571c | ||
|
|
dc9a260425 | ||
|
|
249e6fe637 | ||
|
|
e84b8747a1 | ||
|
|
e548572f12 | ||
|
|
0323a60d85 | ||
|
|
a05cbf8169 | ||
|
|
5aa45607eb | ||
|
|
133c3a330c | ||
|
|
f9646208e9 | ||
|
|
4b793f44ef | ||
|
|
aae4f00a4b | ||
|
|
366a4918c3 | ||
|
|
bc1801fb03 | ||
|
|
25cd12cf33 | ||
|
|
90e22da930 | ||
|
|
e6c7e57711 | ||
|
|
d385bc9cce | ||
|
|
1816ab95a0 | ||
|
|
5c750b6880 | ||
|
|
cd1c137542 | ||
|
|
4cf2ba20c2 | ||
|
|
a1ee2db6d1 | ||
|
|
db8096ccdc | ||
|
|
25a2554bdd | ||
|
|
3bc7024f8b | ||
|
|
4ff80a7074 | ||
|
|
23ee8211c7 | ||
|
|
1e0ff65337 | ||
|
|
da211bef96 | ||
|
|
95342c8c33 | ||
|
|
61e0289014 | ||
|
|
af09861f5c | ||
|
|
7f9e3bc787 | ||
|
|
d995192dde | ||
|
|
c57e19c8fa | ||
|
|
550d9a9f71 | ||
|
|
4208ac2958 | ||
|
|
45429b5400 | ||
|
|
d3916b84c9 | ||
|
|
55d2b9b3c9 | ||
|
|
3b27d41c72 | ||
|
|
044fb9e2f5 | ||
|
|
6007c8705c | ||
|
|
a8c1958c75 | ||
|
|
d696b394c4 | ||
|
|
32c4138758 | ||
|
|
d8bfe77a3b | ||
|
|
8b0969b698 | ||
|
|
66dfa47c66 | ||
|
|
b10255a6dd | ||
|
|
3698e89b88 | ||
|
|
bfa211fb02 | ||
|
|
dc40ac854a | ||
|
|
2b6d041cb6 | ||
|
|
8a58733d91 | ||
|
|
e49b411205 | ||
|
|
08464ee26e | ||
|
|
12ba10bc2c | ||
|
|
dcaf4c905f | ||
|
|
6046ed4f5c | ||
|
|
cf5d89d13c | ||
|
|
9f160537ef | ||
|
|
18e7305b6d | ||
|
|
d9813a5bec | ||
|
|
d7867cd1e2 | ||
|
|
30afc8b1d2 | ||
|
|
32b4b4d24d | ||
|
|
7d32a717af | ||
|
|
892350fa2d | ||
|
|
0db4b29452 | ||
|
|
74ac6eb8a3 | ||
|
|
d9d997b218 | ||
|
|
84c4db13fb | ||
|
|
528fa5c57b | ||
|
|
9a9a35bf40 | ||
|
|
d14dc35efe | ||
|
|
27d784b23e | ||
|
|
9e1f7c4c18 | ||
|
|
77e67c19fe | ||
|
|
91582257fb | ||
|
|
66ef5549e9 | ||
|
|
79e1e1a747 | ||
|
|
0b13c0a437 | ||
|
|
08361eb84e | ||
|
|
3d68fcad0b | ||
|
|
7f44083a96 | ||
|
|
39af2bb0a4 | ||
|
|
9dc292772a | ||
|
|
bf5d9e3224 | ||
|
|
d70014cfd0 | ||
|
|
a785eb9141 | ||
|
|
f52200a340 | ||
|
|
df7ac9b815 | ||
|
|
64a55681e6 | ||
|
|
1d5b665f13 | ||
|
|
51cf6a5ff3 | ||
|
|
e0ff7ba180 | ||
|
|
9ba975d6ad | ||
|
|
1469c02998 | ||
|
|
95e09dd2e9 | ||
|
|
e5e63ed201 | ||
|
|
4212a45767 | ||
|
|
ef01a64826 | ||
|
|
46b4118b9e | ||
|
|
c7fc5f3ab7 | ||
|
|
f50a23accd | ||
|
|
43a1296150 | ||
|
|
3b38641f98 | ||
|
|
8cac89d17c | ||
|
|
92bb9a5fdc | ||
|
|
963f179d7f | ||
|
|
103183f494 | ||
|
|
1cfc2f0c07 | ||
|
|
219715449d | ||
|
|
f011a3df52 | ||
|
|
7adaa2046d | ||
|
|
948871969f | ||
|
|
57707a80e6 | ||
|
|
55da5bc25d | ||
|
|
c718b810f6 | ||
|
|
afd293ee87 | ||
|
|
752bc5dcdd | ||
|
|
973f03e73e | ||
|
|
555c9847d4 | ||
|
|
c1a35a29a8 | ||
|
|
7a6c27cf24 | ||
|
|
d9c1cf9874 | ||
|
|
1155f1b0e1 | ||
|
|
dcc314f088 | ||
|
|
31ff5bffd6 | ||
|
|
4887ea3563 | ||
|
|
dbaaf4216d | ||
|
|
53c25690f9 | ||
|
|
3c12e711a4 | ||
|
|
9b7bd4e9ae | ||
|
|
026b3a1d0f | ||
|
|
d9c08de58a | ||
|
|
c379a6f2fb | ||
|
|
488a3eeace | ||
|
|
4dd9c9e2b9 | ||
|
|
ca0a4bdf8e | ||
|
|
247c7eff14 | ||
|
|
837ec5a27c | ||
|
|
5a15692589 | ||
|
|
0058702749 | ||
|
|
e34ebbc665 | ||
|
|
38a9e6fde1 | ||
|
|
f26ca0866c | ||
|
|
e7ee8a95f6 | ||
|
|
91adefedfa | ||
|
|
2f5eaa8475 | ||
|
|
da964fae93 | ||
|
|
e9c1ad6acd | ||
|
|
f965ee9b1b | ||
|
|
ce940da8e9 | ||
|
|
a8b35eb8f5 | ||
|
|
0c95e5a6ca | ||
|
|
0f39b63801 | ||
|
|
545b5e0161 | ||
|
|
3cf7164a54 | ||
|
|
3f50779a17 | ||
|
|
a8188a2f33 | ||
|
|
d30385f07c | ||
|
|
1b5ff68c43 | ||
|
|
97eabe6f81 | ||
|
|
57a95d1799 | ||
|
|
541dd994a9 | ||
|
|
81a107f503 | ||
|
|
5ab1034698 | ||
|
|
13ba450c4c | ||
|
|
c5470d4050 | ||
|
|
7e49c7d782 | ||
|
|
769a04517f | ||
|
|
768c991909 | ||
|
|
1ee70a0146 | ||
|
|
8be8047b8d | ||
|
|
51b24bbaf3 | ||
|
|
2cb320e246 | ||
|
|
7524f7fbe8 | ||
|
|
9fefb1d898 | ||
|
|
73fc1c1c56 | ||
|
|
d1baff1743 | ||
|
|
dd1cf5c3cf | ||
|
|
9246c11c35 | ||
|
|
0e6002dca2 | ||
|
|
78908bc5cb | ||
|
|
49672bfc5f | ||
|
|
f603d682cd | ||
|
|
b364d404a9 | ||
|
|
96f9c67e77 | ||
|
|
6cebcac805 | ||
|
|
3573896fe0 | ||
|
|
e9a84a21e4 | ||
|
|
25429f760c | ||
|
|
ece4875973 | ||
|
|
2c0547079a | ||
|
|
b3b3a56164 | ||
|
|
4242b45646 | ||
|
|
cab80cbe9d | ||
|
|
d671a8a21d | ||
|
|
6b88ac9c32 | ||
|
|
6ccaf55e54 | ||
|
|
edf29aa67d | ||
|
|
0e6fd645fd | ||
|
|
c63cc78ffd | ||
|
|
3682751455 | ||
|
|
abefa2738b | ||
|
|
4ccd69350b | ||
|
|
0d6880adb3 | ||
|
|
2f368de397 | ||
|
|
650a160f04 | ||
|
|
ecb037fc0e | ||
|
|
8e1bbf32be | ||
|
|
30bb3a109e | ||
|
|
37b6e1cbb7 | ||
|
|
cb83b49432 | ||
|
|
568fec0f54 | ||
|
|
7e2cef98a7 | ||
|
|
90f17d4a28 | ||
|
|
e8dd412ac1 | ||
|
|
54c63063e4 | ||
|
|
58dadad8ec | ||
|
|
e9e558d8c8 | ||
|
|
0897ed561f | ||
|
|
e263805847 | ||
|
|
bfe2205ecb | ||
|
|
04d3ea9563 | ||
|
|
8c47f117db | ||
|
|
36f022bb58 | ||
|
|
e75f56a0f2 | ||
|
|
342a00b89e | ||
|
|
330a71d28b | ||
|
|
ea278b5b12 | ||
|
|
5e7f0c65fe | ||
|
|
b131a2cb98 | ||
|
|
b5a39de3e2 | ||
|
|
42df5ef45e | ||
|
|
b29e295e1b | ||
|
|
8c90157990 | ||
|
|
b454f43b6c | ||
|
|
53194ede5e | ||
|
|
d17d38fe70 | ||
|
|
667fc25766 | ||
|
|
359847d047 | ||
|
|
15567493ba | ||
|
|
591ec02cea | ||
|
|
a1e080d495 | ||
|
|
c2fca054ae | ||
|
|
bf6c2f0dfd | ||
|
|
86ec0b1d9f | ||
|
|
769c330b3d | ||
|
|
5c75450a77 | ||
|
|
ad7c1f3c81 | ||
|
|
23767f734f | ||
|
|
80eaabd360 | ||
|
|
ff5d0f2aeb | ||
|
|
a278428bd5 | ||
|
|
0a491e773b | ||
|
|
45540a00ee | ||
|
|
55f4aa3b34 | ||
|
|
8b63e45f0b | ||
|
|
052cb459a6 | ||
|
|
a7803570dc | ||
|
|
b516ea2fe2 | ||
|
|
1fa45c69d6 | ||
|
|
c4abd93b9b | ||
|
|
91c1768939 | ||
|
|
1a5d6aa498 | ||
|
|
fb69f3d45f | ||
|
|
3fbe93f029 | ||
|
|
df388d9f33 | ||
|
|
0697d08e54 | ||
|
|
895386cfaf | ||
|
|
6a95f9e349 | ||
|
|
ad62a966a6 | ||
|
|
fe4248cf34 | ||
|
|
a237aa8164 | ||
|
|
3dc1e917bf | ||
|
|
27e3e09bb9 | ||
|
|
d1791a999d | ||
|
|
d0b15ed940 | ||
|
|
e4e9da7673 | ||
|
|
8b6e982495 | ||
|
|
71c1e36d1e | ||
|
|
343c426307 | ||
|
|
d8c6adf338 | ||
|
|
e979d75cb8 | ||
|
|
afa7045847 | ||
|
|
e84339ef4a | ||
|
|
fbd6b5b434 | ||
|
|
dc49dec4f0 | ||
|
|
68c37ca2a4 | ||
|
|
1f1c669673 | ||
|
|
d61565d227 | ||
|
|
a5e055f8a5 | ||
|
|
30b105afd5 | ||
|
|
d14e4d41ea | ||
|
|
f54634aeb2 | ||
|
|
5083ab7694 | ||
|
|
48e151495f | ||
|
|
66358f2900 | ||
|
|
5f6334696a | ||
|
|
02a85b1252 | ||
|
|
4628639ac6 | ||
|
|
8440ac3a54 | ||
|
|
1e6ac8caf2 | ||
|
|
7711530704 | ||
|
|
4ffa167256 | ||
|
|
baa07e935e | ||
|
|
c252eae32e | ||
|
|
92d3115f3d | ||
|
|
6bbf614a37 | ||
|
|
ed8b022b51 | ||
|
|
f34c6bd1ce | ||
|
|
c71566e7f5 | ||
|
|
83455028b0 | ||
|
|
d120d0cf2e | ||
|
|
a0416e9c6d | ||
|
|
a53c0b9472 | ||
|
|
3c2b05be90 | ||
|
|
8573c6e8c6 | ||
|
|
7b63369df2 | ||
|
|
997f362cc2 | ||
|
|
59e561dcf9 | ||
|
|
056353f8a8 | ||
|
|
19a9753663 | ||
|
|
66dd0e9ec0 | ||
|
|
d74b8ec4e3 | ||
|
|
dbfa1d7263 | ||
|
|
d090fd25e4 | ||
|
|
1c53b0a1c0 | ||
|
|
a2ac5ae478 | ||
|
|
ead7155b0f | ||
|
|
dfeb702544 | ||
|
|
32f8733313 | ||
|
|
4bf4c780be | ||
|
|
7a7ff4bb96 | ||
|
|
a59da3634b | ||
|
|
a25fcfdfa7 | ||
|
|
2d9db0fed1 | ||
|
|
6ad1f19a21 | ||
|
|
88a32ae48d | ||
|
|
a4f96e6452 | ||
|
|
e27b7d7812 | ||
|
|
ba5d84f7e8 | ||
|
|
ea3a1745f5 | ||
|
|
6d2b27689d | ||
|
|
a8bb3dd9a3 | ||
|
|
d42093e069 | ||
|
|
98482f0150 | ||
|
|
58f4efb579 | ||
|
|
fe10875285 | ||
|
|
e0fe97401d | ||
|
|
f2f507e619 | ||
|
|
f4d4a2f41b | ||
|
|
4ff44dfa3b | ||
|
|
ee16b2051e | ||
|
|
3633f091c5 | ||
|
|
44608517c1 | ||
|
|
841b4d648c | ||
|
|
5b0e333967 | ||
|
|
01b2db4845 | ||
|
|
e7d73b833b | ||
|
|
f7696114bb | ||
|
|
8de67fd9d9 | ||
|
|
be6690bf0b | ||
|
|
a86dc942d6 | ||
|
|
6dcb0bafb0 | ||
|
|
83dae46ec6 | ||
|
|
0cceb3fdf1 | ||
|
|
7885eaf974 | ||
|
|
37d0f06e07 | ||
|
|
2da664ed17 | ||
|
|
2699f170ca | ||
|
|
8406c0d9a3 | ||
|
|
e762f7f3c0 | ||
|
|
65aa4d5642 | ||
|
|
3a9f5d6ddc | ||
|
|
748ad5f05a | ||
|
|
89519b1521 | ||
|
|
7e300079ce | ||
|
|
26f442a675 | ||
|
|
8aa4fbea83 | ||
|
|
2701be91e3 | ||
|
|
f2e87a3429 | ||
|
|
c7a3186d08 | ||
|
|
a5e4ceb735 | ||
|
|
b725cadf48 | ||
|
|
df9a05ba92 | ||
|
|
db1dacde5d | ||
|
|
9f2a9d43b1 | ||
|
|
d6f24feb4a | ||
|
|
40e785fdff | ||
|
|
70a91c5426 | ||
|
|
0a2a5be71c | ||
|
|
4fd4f44bb7 | ||
|
|
378b2fbd9e | ||
|
|
1f9b52d5e1 | ||
|
|
b30bb56483 | ||
|
|
ba22da00bf | ||
|
|
cee5ddee53 | ||
|
|
c74c60a629 | ||
|
|
e4f8fe941e | ||
|
|
b9e1ca1385 | ||
|
|
fd10b49742 | ||
|
|
1c20a8cd31 | ||
|
|
a316e25034 | ||
|
|
f54f2c52e9 | ||
|
|
1adb7fa58c | ||
|
|
dcf6059a15 | ||
|
|
8a6e9f90be | ||
|
|
77af33ba5d | ||
|
|
faabed1df0 | ||
|
|
cae5c76bed | ||
|
|
53b698adb6 | ||
|
|
bbc4673f17 | ||
|
|
dc2733998e | ||
|
|
0d161519e4 | ||
|
|
5dad97779a | ||
|
|
3ba8857491 | ||
|
|
7cd416c63e |
7
.github/pull_request_template.md
vendored
7
.github/pull_request_template.md
vendored
@@ -2,11 +2,4 @@
|
||||
|
||||
Release Notes:
|
||||
|
||||
- N/A
|
||||
|
||||
or
|
||||
|
||||
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
|
||||
|
||||
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
|
||||
These will be removed by the person making the release.
|
||||
|
||||
21
.github/workflows/release_actions.yml
vendored
21
.github/workflows/release_actions.yml
vendored
@@ -6,8 +6,8 @@ jobs:
|
||||
discord_release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get appropriate URL
|
||||
id: get-appropriate-url
|
||||
- name: Get release URL
|
||||
id: get-release-url
|
||||
run: |
|
||||
if [ "${{ github.event.release.prerelease }}" == "true" ]; then
|
||||
URL="https://zed.dev/releases/preview/latest"
|
||||
@@ -15,14 +15,19 @@ jobs:
|
||||
URL="https://zed.dev/releases/stable/latest"
|
||||
fi
|
||||
echo "::set-output name=URL::$URL"
|
||||
- name: Get content
|
||||
uses: 2428392/gh-truncate-string-action@v1.2.0
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it.
|
||||
|
||||
${{ github.event.release.body }}
|
||||
maxLength: 2000
|
||||
- name: Discord Webhook Action
|
||||
uses: tsickert/discord-webhook@v5.3.0
|
||||
with:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
Restart your Zed or head to ${{ steps.get-appropriate-url.outputs.URL }} to grab it.
|
||||
|
||||
${{ github.event.release.body }}
|
||||
content: ${{ steps.get-content.outputs.string }}
|
||||
|
||||
1426
Cargo.lock
generated
1426
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
23
Cargo.toml
23
Cargo.toml
@@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/ai",
|
||||
"crates/assistant",
|
||||
"crates/audio",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -9,6 +10,7 @@ members = [
|
||||
"crates/channel",
|
||||
"crates/cli",
|
||||
"crates/client",
|
||||
"crates/client2",
|
||||
"crates/clock",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
@@ -17,18 +19,23 @@ members = [
|
||||
"crates/component_test",
|
||||
"crates/context_menu",
|
||||
"crates/copilot",
|
||||
"crates/copilot2",
|
||||
"crates/copilot_button",
|
||||
"crates/db",
|
||||
"crates/db2",
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/diagnostics",
|
||||
"crates/drag_and_drop",
|
||||
"crates/editor",
|
||||
"crates/feature_flags",
|
||||
"crates/feature_flags2",
|
||||
"crates/feedback",
|
||||
"crates/file_finder",
|
||||
"crates/fs",
|
||||
"crates/fsevent",
|
||||
"crates/fuzzy",
|
||||
"crates/fuzzy2",
|
||||
"crates/git",
|
||||
"crates/go_to_line",
|
||||
"crates/gpui",
|
||||
@@ -38,11 +45,13 @@ members = [
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language2",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/lsp2",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/node_runtime",
|
||||
@@ -51,7 +60,9 @@ members = [
|
||||
"crates/plugin",
|
||||
"crates/plugin_macros",
|
||||
"crates/plugin_runtime",
|
||||
"crates/prettier",
|
||||
"crates/project",
|
||||
# "crates/project2",
|
||||
"crates/project_panel",
|
||||
"crates/project_symbols",
|
||||
"crates/recent_projects",
|
||||
@@ -59,16 +70,20 @@ members = [
|
||||
"crates/rpc",
|
||||
"crates/search",
|
||||
"crates/settings",
|
||||
"crates/settings2",
|
||||
"crates/snippet",
|
||||
"crates/sqlez",
|
||||
"crates/sqlez_macros",
|
||||
"crates/feature_flags",
|
||||
"crates/storybook",
|
||||
"crates/rich_text",
|
||||
"crates/storybook2",
|
||||
"crates/sum_tree",
|
||||
"crates/terminal",
|
||||
#"crates/terminal2",
|
||||
"crates/text",
|
||||
"crates/theme",
|
||||
"crates/theme2",
|
||||
"crates/theme_selector",
|
||||
"crates/ui2",
|
||||
"crates/util",
|
||||
"crates/semantic_index",
|
||||
"crates/vim",
|
||||
@@ -77,6 +92,7 @@ members = [
|
||||
"crates/welcome",
|
||||
"crates/xtask",
|
||||
"crates/zed",
|
||||
"crates/zed2",
|
||||
"crates/zed-actions"
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
@@ -103,12 +119,14 @@ rand = { version = "0.8.5" }
|
||||
refineable = { path = "./crates/refineable" }
|
||||
regex = { version = "1.5" }
|
||||
rust-embed = { version = "8.0", features = ["include-exclude"] }
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
schemars = { version = "0.8" }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = { version = "1.2" }
|
||||
sysinfo = "0.29.10"
|
||||
tempdir = { version = "0.3.7" }
|
||||
thiserror = { version = "1.0.29" }
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
@@ -117,6 +135,7 @@ tree-sitter = "0.20"
|
||||
unindent = { version = "0.1.7" }
|
||||
pretty_assertions = "1.3.0"
|
||||
git2 = { version = "0.15", default-features = false}
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
|
||||
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" }
|
||||
tree-sitter-c = "0.20.1"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# syntax = docker/dockerfile:1.2
|
||||
|
||||
FROM rust:1.72-bullseye as builder
|
||||
FROM rust:1.73-bullseye as builder
|
||||
WORKDIR app
|
||||
COPY . .
|
||||
|
||||
|
||||
4
Procfile
4
Procfile
@@ -1,4 +1,4 @@
|
||||
web: cd ../zed.dev && PORT=3000 npx vercel dev
|
||||
collab: cd crates/collab && cargo run serve
|
||||
web: cd ../zed.dev && PORT=3000 npm run dev
|
||||
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
|
||||
livekit: livekit-server --dev
|
||||
postgrest: postgrest crates/collab/admin_api.conf
|
||||
|
||||
@@ -13,7 +13,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
sudo xcodebuild -license
|
||||
```
|
||||
|
||||
* Install homebrew, node and rustup-init (rutup, rust, cargo, etc.)
|
||||
* Install homebrew, node and rustup-init (rustup, rust, cargo, etc.)
|
||||
```
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
brew install node rustup-init
|
||||
@@ -36,7 +36,7 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
brew install foreman
|
||||
```
|
||||
|
||||
* Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies:
|
||||
* Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies:
|
||||
|
||||
```
|
||||
cd ..
|
||||
@@ -83,9 +83,7 @@ foreman start
|
||||
If you want to run Zed pointed at the local servers, you can run:
|
||||
|
||||
```
|
||||
script/zed-with-local-servers
|
||||
# or...
|
||||
script/zed-with-local-servers --release
|
||||
script/zed-local
|
||||
```
|
||||
|
||||
### Dump element JSON
|
||||
|
||||
5
assets/icons/stop_sharing.svg
Normal file
5
assets/icons/stop_sharing.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.6 KiB |
@@ -30,6 +30,7 @@
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize",
|
||||
"cmd--": "zed::DecreaseBufferFontSize",
|
||||
"cmd-0": "zed::ResetBufferFontSize",
|
||||
"cmd-,": "zed::OpenSettings",
|
||||
@@ -249,6 +250,7 @@
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ActivateRegexMode",
|
||||
"alt-cmd-s": "search::ActivateSemanticMode",
|
||||
"alt-cmd-x": "search::ActivateTextMode"
|
||||
@@ -261,11 +263,19 @@
|
||||
"down": "search::NextHistoryQuery"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar && in_replace",
|
||||
"bindings": {
|
||||
"enter": "search::ReplaceNext",
|
||||
"cmd-enter": "search::ReplaceAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchView",
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"alt-tab": "search::CycleMode",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-cmd-g": "search::ActivateRegexMode",
|
||||
"alt-cmd-s": "search::ActivateSemanticMode",
|
||||
"alt-cmd-x": "search::ActivateTextMode"
|
||||
@@ -277,6 +287,7 @@
|
||||
"cmd-f": "project_search::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
"cmd-shift-h": "search::ToggleReplace",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-cmd-c": "search::ToggleCaseSensitive",
|
||||
"alt-cmd-w": "search::ToggleWholeWord",
|
||||
@@ -303,7 +314,7 @@
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"cmd-shift-d": "editor::SelectAllMatches",
|
||||
"cmd-shift-l": "editor::SelectAllMatches",
|
||||
"ctrl-cmd-d": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
@@ -463,7 +474,7 @@
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"cmd-shift-d": "editor::DuplicateLine",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
"ctrl-cmd-down": "editor::MoveLineDown",
|
||||
@@ -498,6 +509,22 @@
|
||||
"cmd-k cmd-down": [
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"cmd-k shift-left": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"cmd-k shift-right": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"cmd-k shift-up": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"cmd-k shift-down": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -562,7 +589,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectSearchBar",
|
||||
"context": "ProjectSearchBar && !in_replace",
|
||||
"bindings": {
|
||||
"cmd-enter": "project_search::SearchInNew"
|
||||
}
|
||||
@@ -588,14 +615,20 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel",
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel > Editor",
|
||||
"context": "(CollabPanel && editing) > Editor",
|
||||
"bindings": {
|
||||
"space": "collab_panel::InsertSpace"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "(CollabPanel && not_editing) > Editor",
|
||||
"bindings": {
|
||||
"cmd-c": "collab_panel::StartLinkChannel",
|
||||
"cmd-x": "collab_panel::StartMoveChannel",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"cmd-shift-space": "editor::SelectAll",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-l": "editor::SplitSelectionIntoLines",
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
":": "command_palette::Toggle",
|
||||
"h": "vim::Left",
|
||||
"left": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
@@ -94,6 +95,7 @@
|
||||
}
|
||||
],
|
||||
"ctrl-o": "pane::GoBack",
|
||||
"ctrl-i": "pane::GoForward",
|
||||
"ctrl-]": "editor::GoToDefinition",
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
@@ -125,10 +127,26 @@
|
||||
"g shift-t": "pane::ActivatePrevItem",
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToTypeDefinition",
|
||||
"g n": "vim::SelectNext",
|
||||
"g shift-n": "vim::SelectPrevious",
|
||||
"g >": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"g <": [
|
||||
"editor::SelectPrevious",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"g a": "editor::SelectAllMatches",
|
||||
"g s": "outline::Toggle",
|
||||
"g shift-s": "project_symbols::Toggle",
|
||||
"g .": "editor::ToggleCodeActions", // zed specific
|
||||
"g shift-a": "editor::FindAllReferences", // zed specific
|
||||
"g space": "editor::OpenExcerpts", // zed specific
|
||||
"g *": [
|
||||
"vim::MoveToNext",
|
||||
{
|
||||
@@ -205,13 +223,13 @@
|
||||
"shift-z shift-q": [
|
||||
"pane::CloseActiveItem",
|
||||
{
|
||||
"saveBehavior": "dontSave"
|
||||
"saveIntent": "skip"
|
||||
}
|
||||
],
|
||||
"shift-z shift-z": [
|
||||
"pane::CloseActiveItem",
|
||||
{
|
||||
"saveBehavior": "promptOnConflict"
|
||||
"saveIntent": "saveAll"
|
||||
}
|
||||
],
|
||||
// Count support
|
||||
@@ -300,6 +318,38 @@
|
||||
"workspace::ActivatePaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w shift-left": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w shift-right": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w shift-up": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w shift-down": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w shift-h": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Left"
|
||||
],
|
||||
"ctrl-w shift-l": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Right"
|
||||
],
|
||||
"ctrl-w shift-k": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w shift-j": [
|
||||
"workspace::SwapPaneInDirection",
|
||||
"Down"
|
||||
],
|
||||
"ctrl-w g t": "pane::ActivateNextItem",
|
||||
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
|
||||
"ctrl-w g shift-t": "pane::ActivatePrevItem",
|
||||
@@ -318,7 +368,17 @@
|
||||
"ctrl-w c": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-c": "pane::CloseAllItems",
|
||||
"ctrl-w q": "pane::CloseAllItems",
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems"
|
||||
"ctrl-w ctrl-q": "pane::CloseAllItems",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w n": [
|
||||
"workspace::NewFileInDirection",
|
||||
"Up"
|
||||
],
|
||||
"ctrl-w ctrl-n": [
|
||||
"workspace::NewFileInDirection",
|
||||
"Up"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -348,6 +408,7 @@
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"shift-y": "vim::YankLine",
|
||||
"i": "vim::InsertBefore",
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
@@ -357,6 +418,8 @@
|
||||
"o": "vim::InsertLineBelow",
|
||||
"shift-o": "vim::InsertLineAbove",
|
||||
"~": "vim::ChangeCase",
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"p": "vim::Paste",
|
||||
"shift-p": [
|
||||
"vim::Paste",
|
||||
@@ -468,6 +531,20 @@
|
||||
"shift-r": "vim::SubstituteLine",
|
||||
"c": "vim::Substitute",
|
||||
"~": "vim::ChangeCase",
|
||||
"ctrl-a": "vim::Increment",
|
||||
"ctrl-x": "vim::Decrement",
|
||||
"g ctrl-a": [
|
||||
"vim::Increment",
|
||||
{
|
||||
"step": true
|
||||
}
|
||||
],
|
||||
"g ctrl-x": [
|
||||
"vim::Decrement",
|
||||
{
|
||||
"step": true
|
||||
}
|
||||
],
|
||||
"shift-i": "vim::InsertBefore",
|
||||
"shift-a": "vim::InsertAfter",
|
||||
"shift-j": "vim::JoinLines",
|
||||
@@ -508,11 +585,16 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert && !menu",
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore"
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"ctrl-x ctrl-o": "editor::ShowCompletions",
|
||||
"ctrl-x ctrl-a": "assistant::InlineAssist", // zed specific
|
||||
"ctrl-x ctrl-c": "copilot::Suggest", // zed specific
|
||||
"ctrl-x ctrl-l": "editor::ToggleCodeActions", // zed specific
|
||||
"ctrl-x ctrl-z": "editor::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -531,7 +613,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar > VimEnabled",
|
||||
"context": "BufferSearchBar && !in_replace > VimEnabled",
|
||||
"bindings": {
|
||||
"enter": "vim::SearchSubmit",
|
||||
"escape": "buffer_search::Dismiss"
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
// Settings related to calls in Zed
|
||||
"calls": {
|
||||
// Join calls with the microphone muted by default
|
||||
"mute_on_join": true
|
||||
"mute_on_join": false
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
@@ -199,7 +199,12 @@
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// }
|
||||
"formatter": "language_server",
|
||||
// 3. Format code using Zed's Prettier integration:
|
||||
// "formatter": "prettier"
|
||||
// 4. Default. Format files using Zed's Prettier integration (if applicable),
|
||||
// or falling back to formatting via language server:
|
||||
// "formatter": "auto"
|
||||
"formatter": "auto",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
//
|
||||
@@ -227,6 +232,11 @@
|
||||
},
|
||||
// Automatically update Zed
|
||||
"auto_update": true,
|
||||
// Diagnostics configuration.
|
||||
"diagnostics": {
|
||||
// Whether to show warnings or not by default.
|
||||
"include_warnings": true
|
||||
},
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
@@ -356,7 +366,7 @@
|
||||
".venv",
|
||||
"venv"
|
||||
],
|
||||
// Can also be 'csh' and 'fish'
|
||||
// Can also be 'csh', 'fish', and `nushell`
|
||||
"activate_script": "default"
|
||||
}
|
||||
}
|
||||
@@ -370,7 +380,28 @@
|
||||
},
|
||||
// Difference settings for semantic_index
|
||||
"semantic_index": {
|
||||
"enabled": false
|
||||
"enabled": true
|
||||
},
|
||||
// Settings specific to our elixir integration
|
||||
"elixir": {
|
||||
// Change the LSP zed uses for elixir.
|
||||
// Note that changing this setting requires a restart of Zed
|
||||
// to take effect.
|
||||
//
|
||||
// May take 3 values:
|
||||
// 1. Use the standard ElixirLS, this is the default
|
||||
// "lsp": "elixir_ls"
|
||||
// 2. Use the experimental NextLs
|
||||
// "lsp": "next_ls",
|
||||
// 3. Use a language server installed locally on your machine:
|
||||
// "lsp": {
|
||||
// "local": {
|
||||
// "path": "~/next-ls/bin/start",
|
||||
// "arguments": ["--stdio"]
|
||||
// }
|
||||
// },
|
||||
//
|
||||
"lsp": "elixir_ls"
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
@@ -403,6 +434,16 @@
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// Zed's Prettier integration settings.
|
||||
// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if
|
||||
// project has no other Prettier installed.
|
||||
"prettier": {
|
||||
// Use regular Prettier json configuration:
|
||||
// "trailingComma": "es5",
|
||||
// "tabWidth": 4,
|
||||
// "semi": false,
|
||||
// "singleQuote": true
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
|
||||
@@ -9,39 +9,26 @@ path = "src/ai.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections"}
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
async-trait.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
isahc.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tiktoken-rs = "0.4"
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
log.workspace = true
|
||||
parse_duration = "2.1.1"
|
||||
tiktoken-rs = "0.5.0"
|
||||
matrixmultiply = "0.3.7"
|
||||
rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
|
||||
bincode = "1.3.3"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
||||
@@ -1,294 +1,2 @@
|
||||
pub mod assistant;
|
||||
mod assistant_settings;
|
||||
mod codegen;
|
||||
mod streaming_diff;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
pub use assistant::AssistantPanel;
|
||||
use assistant_settings::OpenAIModel;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
|
||||
use gpui::{executor::Background, AppContext};
|
||||
use isahc::{http::StatusCode, Request, RequestExt};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
fmt::{self, Display},
|
||||
io,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
|
||||
|
||||
// Data types for chat completion requests
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct OpenAIRequest {
|
||||
model: String,
|
||||
messages: Vec<RequestMessage>,
|
||||
stream: bool,
|
||||
}
|
||||
|
||||
#[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(Arc<str>),
|
||||
}
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct RequestMessage {
|
||||
role: Role,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct ResponseMessage {
|
||||
role: Option<Role>,
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn cycle(&mut self) {
|
||||
*self = match self {
|
||||
Role::User => Role::Assistant,
|
||||
Role::Assistant => Role::System,
|
||||
Role::System => Role::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Role::User => write!(f, "User"),
|
||||
Role::Assistant => write!(f, "Assistant"),
|
||||
Role::System => write!(f, "System"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct OpenAIResponseStreamEvent {
|
||||
pub id: Option<String>,
|
||||
pub object: String,
|
||||
pub created: u32,
|
||||
pub model: String,
|
||||
pub choices: Vec<ChatChoiceDelta>,
|
||||
pub usage: Option<Usage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Usage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatChoiceDelta {
|
||||
pub index: u32,
|
||||
pub delta: ResponseMessage,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenAIUsage {
|
||||
prompt_tokens: u64,
|
||||
completion_tokens: u64,
|
||||
total_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenAIChoice {
|
||||
text: String,
|
||||
index: u32,
|
||||
logprobs: Option<serde_json::Value>,
|
||||
finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant::init(cx);
|
||||
}
|
||||
|
||||
pub async fn stream_completion(
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
mut request: OpenAIRequest,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
request.stream = true;
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
|
||||
|
||||
let json_data = serde_json::to_string(&request)?;
|
||||
let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(json_data)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if status == StatusCode::OK {
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||
|
||||
fn parse_line(
|
||||
line: Result<String, io::Error>,
|
||||
) -> Result<Option<OpenAIResponseStreamEvent>> {
|
||||
if let Some(data) = line?.strip_prefix("data: ") {
|
||||
let event = serde_json::from_str(&data)?;
|
||||
Ok(Some(event))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = lines.next().await {
|
||||
if let Some(event) = parse_line(line).transpose() {
|
||||
let done = event.as_ref().map_or(false, |event| {
|
||||
event
|
||||
.choices
|
||||
.last()
|
||||
.map_or(false, |choice| choice.finish_reason.is_some())
|
||||
});
|
||||
if tx.unbounded_send(event).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(rx)
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAIResponse {
|
||||
error: OpenAIError,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAIError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
match serde_json::from_str::<OpenAIResponse>(&body) {
|
||||
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {}",
|
||||
response.error.message,
|
||||
)),
|
||||
|
||||
_ => Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {} {}",
|
||||
response.status(),
|
||||
body,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
pub mod completion;
|
||||
pub mod embedding;
|
||||
|
||||
212
crates/ai/src/completion.rs
Normal file
212
crates/ai/src/completion.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{
|
||||
future::BoxFuture, io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, FutureExt,
|
||||
Stream, StreamExt,
|
||||
};
|
||||
use gpui::executor::Background;
|
||||
use isahc::{http::StatusCode, Request, RequestExt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::{self, Display},
|
||||
io,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn cycle(&mut self) {
|
||||
*self = match self {
|
||||
Role::User => Role::Assistant,
|
||||
Role::Assistant => Role::System,
|
||||
Role::System => Role::User,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Role::User => write!(f, "User"),
|
||||
Role::Assistant => write!(f, "Assistant"),
|
||||
Role::System => write!(f, "System"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct RequestMessage {
|
||||
pub role: Role,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct OpenAIRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
pub struct ResponseMessage {
|
||||
pub role: Option<Role>,
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct OpenAIUsage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ChatChoiceDelta {
|
||||
pub index: u32,
|
||||
pub delta: ResponseMessage,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct OpenAIResponseStreamEvent {
|
||||
pub id: Option<String>,
|
||||
pub object: String,
|
||||
pub created: u32,
|
||||
pub model: String,
|
||||
pub choices: Vec<ChatChoiceDelta>,
|
||||
pub usage: Option<OpenAIUsage>,
|
||||
}
|
||||
|
||||
pub async fn stream_completion(
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
mut request: OpenAIRequest,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
request.stream = true;
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
|
||||
|
||||
let json_data = serde_json::to_string(&request)?;
|
||||
let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(json_data)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if status == StatusCode::OK {
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||
|
||||
fn parse_line(
|
||||
line: Result<String, io::Error>,
|
||||
) -> Result<Option<OpenAIResponseStreamEvent>> {
|
||||
if let Some(data) = line?.strip_prefix("data: ") {
|
||||
let event = serde_json::from_str(&data)?;
|
||||
Ok(Some(event))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = lines.next().await {
|
||||
if let Some(event) = parse_line(line).transpose() {
|
||||
let done = event.as_ref().map_or(false, |event| {
|
||||
event
|
||||
.choices
|
||||
.last()
|
||||
.map_or(false, |choice| choice.finish_reason.is_some())
|
||||
});
|
||||
if tx.unbounded_send(event).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(rx)
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAIResponse {
|
||||
error: OpenAIError,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct OpenAIError {
|
||||
message: String,
|
||||
}
|
||||
|
||||
match serde_json::from_str::<OpenAIResponse>(&body) {
|
||||
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {}",
|
||||
response.error.message,
|
||||
)),
|
||||
|
||||
_ => Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {} {}",
|
||||
response.status(),
|
||||
body,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CompletionProvider {
|
||||
fn complete(
|
||||
&self,
|
||||
prompt: OpenAIRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
|
||||
}
|
||||
|
||||
pub struct OpenAICompletionProvider {
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
}
|
||||
|
||||
impl OpenAICompletionProvider {
|
||||
pub fn new(api_key: String, executor: Arc<Background>) -> Self {
|
||||
Self { api_key, executor }
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for OpenAICompletionProvider {
|
||||
fn complete(
|
||||
&self,
|
||||
prompt: OpenAIRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
|
||||
async move {
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -27,8 +27,30 @@ lazy_static! {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct Embedding(Vec<f32>);
|
||||
pub struct Embedding(pub Vec<f32>);
|
||||
|
||||
// This is needed for semantic index functionality
|
||||
// Unfortunately it has to live wherever the "Embedding" struct is created.
|
||||
// Keeping this in here though, introduces a 'rusqlite' dependency into AI
|
||||
// which is less than ideal
|
||||
impl FromSql for Embedding {
|
||||
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
|
||||
let bytes = value.as_blob()?;
|
||||
let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
|
||||
if embedding.is_err() {
|
||||
return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
|
||||
}
|
||||
Ok(Embedding(embedding.unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Embedding {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
let bytes = bincode::serialize(&self.0)
|
||||
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
|
||||
Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
|
||||
}
|
||||
}
|
||||
impl From<Vec<f32>> for Embedding {
|
||||
fn from(value: Vec<f32>) -> Self {
|
||||
Embedding(value)
|
||||
@@ -63,24 +85,24 @@ impl Embedding {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Embedding {
|
||||
fn column_result(value: ValueRef) -> FromSqlResult<Self> {
|
||||
let bytes = value.as_blob()?;
|
||||
let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
|
||||
if embedding.is_err() {
|
||||
return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
|
||||
}
|
||||
Ok(Embedding(embedding.unwrap()))
|
||||
}
|
||||
}
|
||||
// impl FromSql for Embedding {
|
||||
// fn column_result(value: ValueRef) -> FromSqlResult<Self> {
|
||||
// let bytes = value.as_blob()?;
|
||||
// let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
|
||||
// if embedding.is_err() {
|
||||
// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
|
||||
// }
|
||||
// Ok(Embedding(embedding.unwrap()))
|
||||
// }
|
||||
// }
|
||||
|
||||
impl ToSql for Embedding {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
let bytes = bincode::serialize(&self.0)
|
||||
.map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
|
||||
Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
|
||||
}
|
||||
}
|
||||
// impl ToSql for Embedding {
|
||||
// fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
||||
// let bytes = bincode::serialize(&self.0)
|
||||
// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
|
||||
// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OpenAIEmbeddings {
|
||||
@@ -117,6 +139,7 @@ struct OpenAIEmbeddingUsage {
|
||||
|
||||
#[async_trait]
|
||||
pub trait EmbeddingProvider: Sync + Send {
|
||||
fn is_authenticated(&self) -> bool;
|
||||
async fn embed_batch(&self, spans: Vec<String>) -> Result<Vec<Embedding>>;
|
||||
fn max_tokens_per_batch(&self) -> usize;
|
||||
fn truncate(&self, span: &str) -> (String, usize);
|
||||
@@ -127,6 +150,9 @@ pub struct DummyEmbeddings {}
|
||||
|
||||
#[async_trait]
|
||||
impl EmbeddingProvider for DummyEmbeddings {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
true
|
||||
}
|
||||
fn rate_limit_expiration(&self) -> Option<Instant> {
|
||||
None
|
||||
}
|
||||
@@ -229,6 +255,9 @@ impl OpenAIEmbeddings {
|
||||
|
||||
#[async_trait]
|
||||
impl EmbeddingProvider for OpenAIEmbeddings {
|
||||
fn is_authenticated(&self) -> bool {
|
||||
OPENAI_API_KEY.as_ref().is_some()
|
||||
}
|
||||
fn max_tokens_per_batch(&self) -> usize {
|
||||
50000
|
||||
}
|
||||
48
crates/assistant/Cargo.toml
Normal file
48
crates/assistant/Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "assistant"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ai = { path = "../ai" }
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections"}
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
search = { path = "../search" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
uuid.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 = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
113
crates/assistant/src/assistant.rs
Normal file
113
crates/assistant/src/assistant.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
pub mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod codegen;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
|
||||
use ai::completion::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::AppContext;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
#[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(Arc<str>),
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
use crate::{
|
||||
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
|
||||
codegen::{self, Codegen, CodegenKind, OpenAICompletionProvider},
|
||||
stream_completion, MessageId, MessageMetadata, MessageStatus, OpenAIRequest, RequestMessage,
|
||||
Role, SavedConversation, SavedConversationMetadata, SavedMessage, OPENAI_API_URL,
|
||||
codegen::{self, Codegen, CodegenKind},
|
||||
prompts::generate_content_prompt,
|
||||
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
|
||||
SavedMessage,
|
||||
};
|
||||
use ai::completion::{
|
||||
stream_completion, OpenAICompletionProvider, OpenAIRequest, RequestMessage, OPENAI_API_URL,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Local};
|
||||
@@ -13,7 +17,7 @@ use editor::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
|
||||
},
|
||||
scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset,
|
||||
Anchor, Editor, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
@@ -270,22 +274,40 @@ impl AssistantPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
||||
let selection = editor.read(cx).selections.newest_anchor().clone();
|
||||
if selection.start.excerpt_id() != selection.end.excerpt_id() {
|
||||
return;
|
||||
}
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
|
||||
// Extend the selection to the start and the end of the line.
|
||||
let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
|
||||
if point_selection.end > point_selection.start {
|
||||
point_selection.start.column = 0;
|
||||
// If the selection ends at the start of the line, we don't want to include it.
|
||||
if point_selection.end.column == 0 {
|
||||
point_selection.end.row -= 1;
|
||||
}
|
||||
point_selection.end.column = snapshot.line_len(point_selection.end.row);
|
||||
}
|
||||
|
||||
let codegen_kind = if point_selection.start == point_selection.end {
|
||||
CodegenKind::Generate {
|
||||
position: snapshot.anchor_after(point_selection.start),
|
||||
}
|
||||
} else {
|
||||
CodegenKind::Transform {
|
||||
range: snapshot.anchor_before(point_selection.start)
|
||||
..snapshot.anchor_after(point_selection.end),
|
||||
}
|
||||
};
|
||||
|
||||
let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
|
||||
let provider = Arc::new(OpenAICompletionProvider::new(
|
||||
api_key,
|
||||
cx.background().clone(),
|
||||
));
|
||||
let selection = editor.read(cx).selections.newest_anchor().clone();
|
||||
let codegen_kind = if editor.read(cx).selections.newest::<usize>(cx).is_empty() {
|
||||
CodegenKind::Generate {
|
||||
position: selection.start,
|
||||
}
|
||||
} else {
|
||||
CodegenKind::Transform {
|
||||
range: selection.start..selection.end,
|
||||
}
|
||||
};
|
||||
|
||||
let codegen = cx.add_model(|cx| {
|
||||
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
||||
});
|
||||
@@ -311,7 +333,7 @@ impl AssistantPanel {
|
||||
editor.insert_blocks(
|
||||
[BlockProperties {
|
||||
style: BlockStyle::Flex,
|
||||
position: selection.head().bias_left(&snapshot),
|
||||
position: snapshot.anchor_before(point_selection.head()),
|
||||
height: 2,
|
||||
render: Arc::new({
|
||||
let inline_assistant = inline_assistant.clone();
|
||||
@@ -538,11 +560,26 @@ impl AssistantPanel {
|
||||
self.inline_prompt_history.pop_front();
|
||||
}
|
||||
|
||||
let codegen = pending_assist.codegen.clone();
|
||||
let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
let range = pending_assist.codegen.read(cx).range();
|
||||
let selected_text = snapshot.text_for_range(range.clone()).collect::<String>();
|
||||
let range = codegen.read(cx).range();
|
||||
let start = snapshot.point_to_buffer_offset(range.start);
|
||||
let end = snapshot.point_to_buffer_offset(range.end);
|
||||
let (buffer, range) = if let Some((start, end)) = start.zip(end) {
|
||||
let (start_buffer, start_buffer_offset) = start;
|
||||
let (end_buffer, end_buffer_offset) = end;
|
||||
if start_buffer.remote_id() == end_buffer.remote_id() {
|
||||
(start_buffer.clone(), start_buffer_offset..end_buffer_offset)
|
||||
} else {
|
||||
self.finish_inline_assist(inline_assist_id, false, cx);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
self.finish_inline_assist(inline_assist_id, false, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let language = snapshot.language_at(range.start);
|
||||
let language = buffer.language_at(range.start);
|
||||
let language_name = if let Some(language) = language.as_ref() {
|
||||
if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
|
||||
None
|
||||
@@ -552,95 +589,9 @@ impl AssistantPanel {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language_name = language_name.as_deref();
|
||||
|
||||
let mut prompt = String::new();
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "You're an expert {language_name} engineer.").unwrap();
|
||||
}
|
||||
match pending_assist.codegen.read(cx).kind() {
|
||||
CodegenKind::Transform { .. } => {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You're currently working inside an editor on this file:"
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "```{language_name}").unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "```").unwrap();
|
||||
}
|
||||
for chunk in snapshot.text_for_range(Anchor::min()..Anchor::max()) {
|
||||
write!(prompt, "{chunk}").unwrap();
|
||||
}
|
||||
writeln!(prompt, "```").unwrap();
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"In particular, the user has selected the following text:"
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "```{language_name}").unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "```").unwrap();
|
||||
}
|
||||
writeln!(prompt, "{selected_text}").unwrap();
|
||||
writeln!(prompt, "```").unwrap();
|
||||
writeln!(prompt).unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Modify the selected text given the user prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"You MUST reply only with the edited selected text, not the entire file."
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
CodegenKind::Generate { .. } => {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You're currently working inside an editor on this file:"
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "```{language_name}").unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "```").unwrap();
|
||||
}
|
||||
for chunk in snapshot.text_for_range(Anchor::min()..range.start) {
|
||||
write!(prompt, "{chunk}").unwrap();
|
||||
}
|
||||
write!(prompt, "<|>").unwrap();
|
||||
for chunk in snapshot.text_for_range(range.start..Anchor::max()) {
|
||||
write!(prompt, "{chunk}").unwrap();
|
||||
}
|
||||
writeln!(prompt).unwrap();
|
||||
writeln!(prompt, "```").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|>` marker is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Text can't be replaced, so assume your answer will be inserted at the cursor."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Complete the text given the user prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "Your answer MUST always be valid {language_name}.").unwrap();
|
||||
}
|
||||
writeln!(prompt, "Always wrap your response in a Markdown codeblock.").unwrap();
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
let codegen_kind = codegen.read(cx).kind().clone();
|
||||
let user_prompt = user_prompt.to_string();
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut model = settings::get::<AssistantSettings>(cx)
|
||||
@@ -657,18 +608,26 @@ impl AssistantPanel {
|
||||
model = conversation.model.clone();
|
||||
}
|
||||
|
||||
messages.push(RequestMessage {
|
||||
role: Role::User,
|
||||
content: prompt,
|
||||
let prompt = cx.background().spawn(async move {
|
||||
let language_name = language_name.as_deref();
|
||||
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
|
||||
});
|
||||
let request = OpenAIRequest {
|
||||
model: model.full_name().into(),
|
||||
messages,
|
||||
stream: true,
|
||||
};
|
||||
pending_assist
|
||||
.codegen
|
||||
.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
|
||||
messages.push(RequestMessage {
|
||||
role: Role::User,
|
||||
content: prompt,
|
||||
});
|
||||
let request = OpenAIRequest {
|
||||
model: model.full_name().into(),
|
||||
messages,
|
||||
stream: true,
|
||||
};
|
||||
codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx));
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_highlights_for_editor(
|
||||
@@ -1,59 +1,12 @@
|
||||
use crate::{
|
||||
stream_completion,
|
||||
streaming_diff::{Hunk, StreamingDiff},
|
||||
OpenAIRequest,
|
||||
};
|
||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, OpenAIRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{
|
||||
multi_buffer, Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
use futures::{
|
||||
channel::mpsc, future::BoxFuture, stream::BoxStream, FutureExt, SinkExt, Stream, StreamExt,
|
||||
};
|
||||
use gpui::{executor::Background, Entity, ModelContext, ModelHandle, Task};
|
||||
use editor::{multi_buffer, Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{Entity, ModelContext, ModelHandle, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use std::{cmp, future, ops::Range, sync::Arc};
|
||||
|
||||
pub trait CompletionProvider {
|
||||
fn complete(
|
||||
&self,
|
||||
prompt: OpenAIRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>>;
|
||||
}
|
||||
|
||||
pub struct OpenAICompletionProvider {
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
}
|
||||
|
||||
impl OpenAICompletionProvider {
|
||||
pub fn new(api_key: String, executor: Arc<Background>) -> Self {
|
||||
Self { api_key, executor }
|
||||
}
|
||||
}
|
||||
|
||||
impl CompletionProvider for OpenAICompletionProvider {
|
||||
fn complete(
|
||||
&self,
|
||||
prompt: OpenAIRequest,
|
||||
) -> BoxFuture<'static, Result<BoxStream<'static, Result<String>>>> {
|
||||
let request = stream_completion(self.api_key.clone(), self.executor.clone(), prompt);
|
||||
async move {
|
||||
let response = request.await?;
|
||||
let stream = response
|
||||
.filter_map(|response| async move {
|
||||
match response {
|
||||
Ok(mut response) => Some(Ok(response.choices.pop()?.delta.content?)),
|
||||
Err(error) => Some(Err(error)),
|
||||
}
|
||||
})
|
||||
.boxed();
|
||||
Ok(stream)
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
@@ -85,26 +38,11 @@ impl Entity for Codegen {
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
mut kind: CodegenKind,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
match &mut kind {
|
||||
CodegenKind::Transform { range } => {
|
||||
let mut point_range = range.to_point(&snapshot);
|
||||
point_range.start.column = 0;
|
||||
if point_range.end.column > 0 || point_range.start.row == point_range.end.row {
|
||||
point_range.end.column = snapshot.line_len(point_range.end.row);
|
||||
}
|
||||
range.start = snapshot.anchor_before(point_range.start);
|
||||
range.end = snapshot.anchor_after(point_range.end);
|
||||
}
|
||||
CodegenKind::Generate { position } => {
|
||||
*position = position.bias_right(&snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
@@ -397,13 +335,17 @@ fn strip_markdown_codeblock(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::stream;
|
||||
use futures::{
|
||||
future::BoxFuture,
|
||||
stream::{self, BoxStream},
|
||||
};
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use parking_lot::Mutex;
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use smol::future::FutureExt;
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(
|
||||
@@ -427,7 +369,7 @@ mod tests {
|
||||
let buffer = cx.add_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, 4))..snapshot.anchor_after(Point::new(4, 4))
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let provider = Arc::new(TestCompletionProvider::new());
|
||||
let codegen = cx.add_model(|cx| {
|
||||
418
crates/assistant/src/prompts.rs
Normal file
418
crates/assistant/src/prompts.rs
Normal file
@@ -0,0 +1,418 @@
|
||||
use crate::codegen::CodegenKind;
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::fmt::Write;
|
||||
use std::ops::Range;
|
||||
|
||||
#[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("<|START|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|END|>");
|
||||
}
|
||||
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("<|START|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|END|>");
|
||||
}
|
||||
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<impl ToOffset>,
|
||||
kind: CodegenKind,
|
||||
) -> String {
|
||||
let range = range.to_offset(buffer);
|
||||
let mut prompt = String::new();
|
||||
|
||||
// General Preamble
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "You're an expert engineer.\n").unwrap();
|
||||
}
|
||||
|
||||
let mut content = String::new();
|
||||
content.extend(buffer.text_for_range(0..range.start));
|
||||
if range.start == range.end {
|
||||
content.push_str("<|START|>");
|
||||
} else {
|
||||
content.push_str("<|START|");
|
||||
}
|
||||
content.extend(buffer.text_for_range(range.clone()));
|
||||
if range.start != range.end {
|
||||
content.push_str("|END|>");
|
||||
}
|
||||
content.extend(buffer.text_for_range(range.end..buffer.len()));
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"The file you are currently working on has the following content:"
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(language_name) = language_name {
|
||||
let language_name = language_name.to_lowercase();
|
||||
writeln!(prompt, "```{language_name}\n{content}\n```").unwrap();
|
||||
} else {
|
||||
writeln!(prompt, "```\n{content}\n```").unwrap();
|
||||
}
|
||||
|
||||
match kind {
|
||||
CodegenKind::Generate { position: _ } => {
|
||||
writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|START|` marker is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Text can't be replaced, so assume your answer will be inserted at the cursor."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate text based on the users prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
CodegenKind::Transform { range: _ } => {
|
||||
writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Modify the users code selected text based upon the users prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file."
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(language_name) = language_name {
|
||||
writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap();
|
||||
}
|
||||
writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap();
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::AppContext;
|
||||
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) {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
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.add_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 {
|
||||
<|START|>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 <|START|a |END|>= 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 {
|
||||
<|START|>
|
||||
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 {}
|
||||
}
|
||||
<|START|>"}
|
||||
);
|
||||
|
||||
// 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! {"
|
||||
<|START|>struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -115,13 +115,15 @@ pub fn check(_: &Check, cx: &mut AppContext) {
|
||||
|
||||
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
if let Some(auto_updater) = AutoUpdater::get(cx) {
|
||||
let server_url = &auto_updater.read(cx).server_url;
|
||||
let auto_updater = auto_updater.read(cx);
|
||||
let server_url = &auto_updater.server_url;
|
||||
let current_version = auto_updater.current_version;
|
||||
let latest_release_url = if cx.has_global::<ReleaseChannel>()
|
||||
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
{
|
||||
format!("{server_url}/releases/preview/latest")
|
||||
format!("{server_url}/releases/preview/{current_version}")
|
||||
} else {
|
||||
format!("{server_url}/releases/stable/latest")
|
||||
format!("{server_url}/releases/stable/{current_version}")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ test-support = [
|
||||
|
||||
[dependencies]
|
||||
audio = { path = "../audio" }
|
||||
channel = { path = "../channel" }
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
||||
@@ -2,22 +2,22 @@ pub mod call_settings;
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use audio::Audio;
|
||||
use call_settings::CallSettings;
|
||||
use channel::ChannelId;
|
||||
use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
|
||||
use client::{
|
||||
proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore,
|
||||
ZED_ALWAYS_ACTIVE,
|
||||
};
|
||||
use collections::HashSet;
|
||||
use futures::{future::Shared, FutureExt};
|
||||
use postage::watch;
|
||||
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
|
||||
WeakModelHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use participant::ParticipantLocation;
|
||||
pub use room::Room;
|
||||
@@ -68,6 +68,7 @@ impl ActiveCall {
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
|
||||
_subscriptions: vec![
|
||||
client.add_request_handler(cx.handle(), Self::handle_incoming_call),
|
||||
client.add_message_handler(cx.handle(), Self::handle_call_canceled),
|
||||
@@ -77,7 +78,7 @@ impl ActiveCall {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
|
||||
pub fn channel_id(&self, cx: &AppContext) -> Option<u64> {
|
||||
self.room()?.read(cx).channel_id()
|
||||
}
|
||||
|
||||
@@ -206,9 +207,14 @@ impl ActiveCall {
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
if result.is_ok() {
|
||||
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx));
|
||||
} else {
|
||||
// TODO: Resport collaboration error
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
this.report_call_event("invite", cx);
|
||||
cx.notify();
|
||||
});
|
||||
result
|
||||
@@ -273,13 +279,7 @@ impl ActiveCall {
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
Self::report_call_event_for_room(
|
||||
"decline incoming",
|
||||
Some(call.room_id),
|
||||
None,
|
||||
&self.client,
|
||||
cx,
|
||||
);
|
||||
report_call_event_for_room("decline incoming", call.room_id, None, &self.client, cx);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
@@ -290,10 +290,10 @@ impl ActiveCall {
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
) -> Task<Result<ModelHandle<Room>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(()));
|
||||
return Task::ready(Ok(room));
|
||||
} else {
|
||||
room.update(cx, |room, cx| room.clear_state(cx));
|
||||
}
|
||||
@@ -308,7 +308,7 @@ impl ActiveCall {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("join channel", cx)
|
||||
});
|
||||
Ok(())
|
||||
Ok(room)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -349,17 +349,22 @@ impl ActiveCall {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn location(&self) -> Option<&WeakModelHandle<Project>> {
|
||||
self.location.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
room.update(cx, |room, cx| room.set_location(project, cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
|
||||
self.location = project.map(|project| project.downgrade());
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
return room.update(cx, |room, cx| room.set_location(project, cx));
|
||||
}
|
||||
}
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_room(
|
||||
@@ -409,31 +414,46 @@ impl ActiveCall {
|
||||
&self.pending_invites
|
||||
}
|
||||
|
||||
fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
|
||||
let (room_id, channel_id) = match self.room() {
|
||||
Some(room) => {
|
||||
let room = room.read(cx);
|
||||
(Some(room.id()), room.channel_id())
|
||||
}
|
||||
None => (None, None),
|
||||
};
|
||||
Self::report_call_event_for_room(operation, room_id, channel_id, &self.client, cx)
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: Option<u64>,
|
||||
channel_id: Option<u64>,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
let event = ClickhouseEvent::Call {
|
||||
operation,
|
||||
room_id,
|
||||
channel_id,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
let room = room.read(cx);
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_room(
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
channel_id: Option<u64>,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
let event = ClickhouseEvent::Call {
|
||||
operation,
|
||||
room_id: Some(room_id),
|
||||
channel_id,
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
}
|
||||
|
||||
pub fn report_call_event_for_channel(
|
||||
operation: &'static str,
|
||||
channel_id: u64,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
|
||||
let telemetry = client.telemetry();
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
|
||||
let event = ClickhouseEvent::Call {
|
||||
operation,
|
||||
room_id: room.map(|r| r.read(cx).id()),
|
||||
channel_id: Some(channel_id),
|
||||
};
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::ParticipantIndex;
|
||||
use client::{proto, User};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakModelHandle;
|
||||
@@ -43,6 +44,7 @@ pub struct RemoteParticipant {
|
||||
pub peer_id: proto::PeerId,
|
||||
pub projects: Vec<proto::ParticipantProject>,
|
||||
pub location: ParticipantLocation,
|
||||
pub participant_index: ParticipantIndex,
|
||||
pub muted: bool,
|
||||
pub speaking: bool,
|
||||
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
|
||||
|
||||
@@ -7,7 +7,7 @@ use anyhow::{anyhow, Result};
|
||||
use audio::{Audio, Sound};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, TypedEnvelope, User, UserStore,
|
||||
Client, ParticipantIndex, TypedEnvelope, User, UserStore,
|
||||
};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
@@ -18,7 +18,7 @@ use live_kit_client::{
|
||||
LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate,
|
||||
RemoteVideoTrackUpdate,
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
use project::Project;
|
||||
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
@@ -44,6 +44,12 @@ pub enum Event {
|
||||
RemoteProjectUnshared {
|
||||
project_id: u64,
|
||||
},
|
||||
RemoteProjectJoined {
|
||||
project_id: u64,
|
||||
},
|
||||
RemoteProjectInvitationDiscarded {
|
||||
project_id: u64,
|
||||
},
|
||||
Left,
|
||||
}
|
||||
|
||||
@@ -64,6 +70,8 @@ pub struct Room {
|
||||
user_store: ModelHandle<UserStore>,
|
||||
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
|
||||
subscriptions: Vec<client::Subscription>,
|
||||
room_update_completed_tx: watch::Sender<Option<()>>,
|
||||
room_update_completed_rx: watch::Receiver<Option<()>>,
|
||||
pending_room_update: Option<Task<()>>,
|
||||
maintain_connection: Option<Task<Option<()>>>,
|
||||
}
|
||||
@@ -98,6 +106,10 @@ impl Room {
|
||||
self.channel_id
|
||||
}
|
||||
|
||||
pub fn is_sharing_project(&self) -> bool {
|
||||
!self.shared_projects.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn is_connected(&self) -> bool {
|
||||
if let Some(live_kit) = self.live_kit.as_ref() {
|
||||
@@ -201,6 +213,8 @@ impl Room {
|
||||
|
||||
Audio::play_sound(Sound::Joined, cx);
|
||||
|
||||
let (room_update_completed_tx, room_update_completed_rx) = watch::channel();
|
||||
|
||||
Self {
|
||||
id,
|
||||
channel_id,
|
||||
@@ -220,6 +234,8 @@ impl Room {
|
||||
user_store,
|
||||
follows_by_leader_id_project_id: Default::default(),
|
||||
maintain_connection: Some(maintain_connection),
|
||||
room_update_completed_tx,
|
||||
room_update_completed_rx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,6 +604,43 @@ impl Room {
|
||||
.map_or(&[], |v| v.as_slice())
|
||||
}
|
||||
|
||||
/// Returns the most 'active' projects, defined as most people in the project
|
||||
pub fn most_active_project(&self, cx: &AppContext) -> Option<(u64, u64)> {
|
||||
let mut project_hosts_and_guest_counts = HashMap::<u64, (Option<u64>, u32)>::default();
|
||||
for participant in self.remote_participants.values() {
|
||||
match participant.location {
|
||||
ParticipantLocation::SharedProject { project_id } => {
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project_id)
|
||||
.or_default()
|
||||
.1 += 1;
|
||||
}
|
||||
ParticipantLocation::External | ParticipantLocation::UnsharedProject => {}
|
||||
}
|
||||
for project in &participant.projects {
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project.id)
|
||||
.or_default()
|
||||
.0 = Some(participant.user.id);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(user) = self.user_store.read(cx).current_user() {
|
||||
for project in &self.local_participant.projects {
|
||||
project_hosts_and_guest_counts
|
||||
.entry(project.id)
|
||||
.or_default()
|
||||
.0 = Some(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
project_hosts_and_guest_counts
|
||||
.into_iter()
|
||||
.filter_map(|(id, (host, guest_count))| Some((id, host?, guest_count)))
|
||||
.max_by_key(|(_, _, guest_count)| *guest_count)
|
||||
.map(|(id, host, _)| (id, host))
|
||||
}
|
||||
|
||||
async fn handle_room_updated(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::RoomUpdated>,
|
||||
@@ -651,6 +704,7 @@ impl Room {
|
||||
let Some(peer_id) = participant.peer_id else {
|
||||
continue;
|
||||
};
|
||||
let participant_index = ParticipantIndex(participant.participant_index);
|
||||
this.participant_user_ids.insert(participant.user_id);
|
||||
|
||||
let old_projects = this
|
||||
@@ -701,8 +755,9 @@ impl Room {
|
||||
if let Some(remote_participant) =
|
||||
this.remote_participants.get_mut(&participant.user_id)
|
||||
{
|
||||
remote_participant.projects = participant.projects;
|
||||
remote_participant.peer_id = peer_id;
|
||||
remote_participant.projects = participant.projects;
|
||||
remote_participant.participant_index = participant_index;
|
||||
if location != remote_participant.location {
|
||||
remote_participant.location = location;
|
||||
cx.emit(Event::ParticipantLocationChanged {
|
||||
@@ -714,6 +769,7 @@ impl Room {
|
||||
participant.user_id,
|
||||
RemoteParticipant {
|
||||
user: user.clone(),
|
||||
participant_index,
|
||||
peer_id,
|
||||
projects: participant.projects,
|
||||
location,
|
||||
@@ -807,7 +863,17 @@ impl Room {
|
||||
let _ = this.leave(cx);
|
||||
}
|
||||
|
||||
this.user_store.update(cx, |user_store, cx| {
|
||||
let participant_indices_by_user_id = this
|
||||
.remote_participants
|
||||
.iter()
|
||||
.map(|(user_id, participant)| (*user_id, participant.participant_index))
|
||||
.collect();
|
||||
user_store.set_participant_indices(participant_indices_by_user_id, cx);
|
||||
});
|
||||
|
||||
this.check_invariants();
|
||||
this.room_update_completed_tx.try_send(Some(())).ok();
|
||||
cx.notify();
|
||||
});
|
||||
}));
|
||||
@@ -816,6 +882,17 @@ impl Room {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn room_update_completed(&mut self) -> impl Future<Output = ()> {
|
||||
let mut done_rx = self.room_update_completed_rx.clone();
|
||||
async move {
|
||||
while let Some(result) = done_rx.next().await {
|
||||
if result.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_video_track_updated(
|
||||
&mut self,
|
||||
change: RemoteVideoTrackUpdate,
|
||||
@@ -1003,6 +1080,7 @@ impl Room {
|
||||
) -> Task<Result<ModelHandle<Project>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
cx.emit(Event::RemoteProjectJoined { project_id: id });
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let project =
|
||||
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||
|
||||
@@ -23,6 +23,7 @@ language = { path = "../language" }
|
||||
settings = { path = "../settings" }
|
||||
feature_flags = { path = "../feature_flags" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
clock = { path = "../clock" }
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
@@ -38,7 +39,7 @@ smol.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
uuid.workspace = true
|
||||
url = "2.2"
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
|
||||
@@ -2,19 +2,21 @@ mod channel_buffer;
|
||||
mod channel_chat;
|
||||
mod channel_store;
|
||||
|
||||
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent};
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{AppContext, ModelHandle};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use channel_buffer::{ChannelBuffer, ChannelBufferEvent, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
|
||||
pub use channel_chat::{ChannelChat, ChannelChatEvent, ChannelMessage, ChannelMessageId};
|
||||
pub use channel_store::{
|
||||
Channel, ChannelData, ChannelEvent, ChannelId, ChannelMembership, ChannelPath, ChannelStore,
|
||||
};
|
||||
|
||||
use client::Client;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod channel_store_tests;
|
||||
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
|
||||
channel_store::init(client, user_store, cx);
|
||||
channel_buffer::init(client);
|
||||
channel_chat::init(client);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
use crate::Channel;
|
||||
use anyhow::Result;
|
||||
use client::Client;
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle};
|
||||
use rpc::{proto, TypedEnvelope};
|
||||
use std::sync::Arc;
|
||||
use client::{Client, Collaborator, UserStore};
|
||||
use collections::HashMap;
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use language::proto::serialize_version;
|
||||
use rpc::{
|
||||
proto::{self, PeerId},
|
||||
TypedEnvelope,
|
||||
};
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use util::ResultExt;
|
||||
|
||||
pub const ACKNOWLEDGE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(250);
|
||||
|
||||
pub(crate) fn init(client: &Arc<Client>) {
|
||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer);
|
||||
client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator);
|
||||
client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator);
|
||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborator);
|
||||
client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer_collaborators);
|
||||
}
|
||||
|
||||
pub struct ChannelBuffer {
|
||||
pub(crate) channel: Arc<Channel>,
|
||||
connected: bool,
|
||||
collaborators: Vec<proto::Collaborator>,
|
||||
collaborators: HashMap<PeerId, Collaborator>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
buffer: ModelHandle<language::Buffer>,
|
||||
buffer_epoch: u64,
|
||||
client: Arc<Client>,
|
||||
subscription: Option<client::Subscription>,
|
||||
acknowledge_task: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
pub enum ChannelBufferEvent {
|
||||
CollaboratorsChanged,
|
||||
Disconnected,
|
||||
BufferEdited,
|
||||
}
|
||||
|
||||
impl Entity for ChannelBuffer {
|
||||
@@ -33,6 +41,9 @@ impl Entity for ChannelBuffer {
|
||||
|
||||
fn release(&mut self, _: &mut AppContext) {
|
||||
if self.connected {
|
||||
if let Some(task) = self.acknowledge_task.take() {
|
||||
task.detach();
|
||||
}
|
||||
self.client
|
||||
.send(proto::LeaveChannelBuffer {
|
||||
channel_id: self.channel.id,
|
||||
@@ -46,6 +57,7 @@ impl ChannelBuffer {
|
||||
pub(crate) async fn new(
|
||||
channel: Arc<Channel>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<ModelHandle<Self>> {
|
||||
let response = client
|
||||
@@ -61,8 +73,6 @@ impl ChannelBuffer {
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let collaborators = response.collaborators;
|
||||
|
||||
let buffer = cx.add_model(|_| {
|
||||
language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text)
|
||||
});
|
||||
@@ -73,34 +83,50 @@ impl ChannelBuffer {
|
||||
anyhow::Ok(cx.add_model(|cx| {
|
||||
cx.subscribe(&buffer, Self::on_buffer_update).detach();
|
||||
|
||||
Self {
|
||||
let mut this = Self {
|
||||
buffer,
|
||||
buffer_epoch: response.epoch,
|
||||
client,
|
||||
connected: true,
|
||||
collaborators,
|
||||
collaborators: Default::default(),
|
||||
acknowledge_task: None,
|
||||
channel,
|
||||
subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())),
|
||||
}
|
||||
user_store,
|
||||
};
|
||||
this.replace_collaborators(response.collaborators, cx);
|
||||
this
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn remote_id(&self, cx: &AppContext) -> u64 {
|
||||
self.buffer.read(cx).remote_id()
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> &ModelHandle<UserStore> {
|
||||
&self.user_store
|
||||
}
|
||||
|
||||
pub(crate) fn replace_collaborators(
|
||||
&mut self,
|
||||
collaborators: Vec<proto::Collaborator>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
for old_collaborator in &self.collaborators {
|
||||
if collaborators
|
||||
.iter()
|
||||
.any(|c| c.replica_id == old_collaborator.replica_id)
|
||||
{
|
||||
let mut new_collaborators = HashMap::default();
|
||||
for collaborator in collaborators {
|
||||
if let Ok(collaborator) = Collaborator::from_proto(collaborator) {
|
||||
new_collaborators.insert(collaborator.peer_id, collaborator);
|
||||
}
|
||||
}
|
||||
|
||||
for (_, old_collaborator) in &self.collaborators {
|
||||
if !new_collaborators.contains_key(&old_collaborator.peer_id) {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.remove_peer(old_collaborator.replica_id as u16, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
self.collaborators = collaborators;
|
||||
self.collaborators = new_collaborators;
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -127,64 +153,14 @@ impl ChannelBuffer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_add_channel_buffer_collaborator(
|
||||
async fn handle_update_channel_buffer_collaborators(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::AddChannelBufferCollaborator>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let collaborator = envelope.payload.collaborator.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Should have gotten a collaborator in the AddChannelBufferCollaborator message"
|
||||
)
|
||||
})?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.collaborators.push(collaborator);
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_remove_channel_buffer_collaborator(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::RemoveChannelBufferCollaborator>,
|
||||
message: TypedEnvelope<proto::UpdateChannelBufferCollaborators>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.collaborators.retain(|collaborator| {
|
||||
if collaborator.peer_id == message.payload.peer_id {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.remove_peer(collaborator.replica_id as u16, cx)
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_update_channel_buffer_collaborator(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::UpdateChannelBufferCollaborator>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for collaborator in &mut this.collaborators {
|
||||
if collaborator.peer_id == message.payload.old_peer_id {
|
||||
collaborator.peer_id = message.payload.new_peer_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.replace_collaborators(message.payload.collaborators, cx);
|
||||
cx.emit(ChannelBufferEvent::CollaboratorsChanged);
|
||||
cx.notify();
|
||||
});
|
||||
@@ -196,19 +172,45 @@ impl ChannelBuffer {
|
||||
&mut self,
|
||||
_: ModelHandle<language::Buffer>,
|
||||
event: &language::Event,
|
||||
_: &mut ModelContext<Self>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let language::Event::Operation(operation) = event {
|
||||
let operation = language::proto::serialize_operation(operation);
|
||||
self.client
|
||||
.send(proto::UpdateChannelBuffer {
|
||||
channel_id: self.channel.id,
|
||||
operations: vec![operation],
|
||||
})
|
||||
.log_err();
|
||||
match event {
|
||||
language::Event::Operation(operation) => {
|
||||
let operation = language::proto::serialize_operation(operation);
|
||||
self.client
|
||||
.send(proto::UpdateChannelBuffer {
|
||||
channel_id: self.channel.id,
|
||||
operations: vec![operation],
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
language::Event::Edited => {
|
||||
cx.emit(ChannelBufferEvent::BufferEdited);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_buffer_version(&mut self, cx: &mut ModelContext<'_, ChannelBuffer>) {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let version = buffer.version();
|
||||
let buffer_id = buffer.remote_id();
|
||||
let client = self.client.clone();
|
||||
let epoch = self.epoch();
|
||||
|
||||
self.acknowledge_task = Some(cx.spawn_weak(|_, cx| async move {
|
||||
cx.background().timer(ACKNOWLEDGE_DEBOUNCE_INTERVAL).await;
|
||||
client
|
||||
.send(proto::AckBufferOperation {
|
||||
buffer_id,
|
||||
epoch,
|
||||
version: serialize_version(&version),
|
||||
})
|
||||
.ok();
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn epoch(&self) -> u64 {
|
||||
self.buffer_epoch
|
||||
}
|
||||
@@ -217,7 +219,7 @@ impl ChannelBuffer {
|
||||
self.buffer.clone()
|
||||
}
|
||||
|
||||
pub fn collaborators(&self) -> &[proto::Collaborator] {
|
||||
pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
|
||||
&self.collaborators
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::Channel;
|
||||
use crate::{Channel, ChannelId, ChannelStore};
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{
|
||||
proto,
|
||||
@@ -16,7 +16,9 @@ use util::{post_inc, ResultExt as _, TryFutureExt};
|
||||
pub struct ChannelChat {
|
||||
channel: Arc<Channel>,
|
||||
messages: SumTree<ChannelMessage>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
loaded_all_messages: bool,
|
||||
last_acknowledged_id: Option<u64>,
|
||||
next_pending_message_id: usize,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
@@ -34,7 +36,7 @@ pub struct ChannelMessage {
|
||||
pub nonce: u128,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ChannelMessageId {
|
||||
Saved(u64),
|
||||
Pending(usize),
|
||||
@@ -55,6 +57,10 @@ pub enum ChannelChatEvent {
|
||||
old_range: Range<usize>,
|
||||
new_count: usize,
|
||||
},
|
||||
NewMessage {
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
@@ -77,6 +83,7 @@ impl Entity for ChannelChat {
|
||||
impl ChannelChat {
|
||||
pub async fn new(
|
||||
channel: Arc<Channel>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
client: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
@@ -94,11 +101,13 @@ impl ChannelChat {
|
||||
let mut this = Self {
|
||||
channel,
|
||||
user_store,
|
||||
channel_store,
|
||||
rpc: client,
|
||||
outgoing_messages_lock: Default::default(),
|
||||
messages: Default::default(),
|
||||
loaded_all_messages,
|
||||
next_pending_message_id: 0,
|
||||
last_acknowledged_id: None,
|
||||
rng: StdRng::from_entropy(),
|
||||
_subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()),
|
||||
};
|
||||
@@ -219,6 +228,26 @@ impl ChannelChat {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn acknowledge_last_message(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let ChannelMessageId::Saved(latest_message_id) = self.messages.summary().max_id {
|
||||
if self
|
||||
.last_acknowledged_id
|
||||
.map_or(true, |acknowledged_id| acknowledged_id < latest_message_id)
|
||||
{
|
||||
self.rpc
|
||||
.send(proto::AckChannelMessage {
|
||||
channel_id: self.channel.id,
|
||||
message_id: latest_message_id,
|
||||
})
|
||||
.ok();
|
||||
self.last_acknowledged_id = Some(latest_message_id);
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store.acknowledge_message_id(self.channel.id, latest_message_id, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
@@ -313,10 +342,15 @@ impl ChannelChat {
|
||||
.payload
|
||||
.message
|
||||
.ok_or_else(|| anyhow!("empty message"))?;
|
||||
let message_id = message.id;
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx)
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
cx.emit(ChannelChatEvent::NewMessage {
|
||||
channel_id: this.channel.id,
|
||||
message_id,
|
||||
})
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -388,6 +422,7 @@ impl ChannelChat {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ mod channel_index;
|
||||
|
||||
use crate::{channel_buffer::ChannelBuffer, channel_chat::ChannelChat};
|
||||
use anyhow::{anyhow, Result};
|
||||
use channel_index::ChannelIndex;
|
||||
use client::{Client, Subscription, User, UserId, UserStore};
|
||||
use collections::{
|
||||
hash_map::{self, DefaultHasher},
|
||||
HashMap, HashSet,
|
||||
};
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use db::RELEASE_CHANNEL;
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||
use rpc::{
|
||||
@@ -14,17 +13,14 @@ use rpc::{
|
||||
TypedEnvelope,
|
||||
};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
hash::{Hash, Hasher},
|
||||
mem,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{borrow::Cow, hash::Hash, mem, ops::Deref, sync::Arc, time::Duration};
|
||||
use util::ResultExt;
|
||||
|
||||
use self::channel_index::ChannelIndex;
|
||||
pub fn init(client: &Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
cx.set_global(channel_store);
|
||||
}
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
@@ -53,6 +49,28 @@ pub type ChannelData = (Channel, ChannelPath);
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||
pub unseen_message_id: Option<u64>,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn link(&self) -> String {
|
||||
RELEASE_CHANNEL.link_prefix().to_owned()
|
||||
+ "channel/"
|
||||
+ &self.slug()
|
||||
+ "-"
|
||||
+ &self.id.to_string()
|
||||
}
|
||||
|
||||
pub fn slug(&self) -> String {
|
||||
let slug: String = self
|
||||
.name
|
||||
.chars()
|
||||
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||
.collect();
|
||||
|
||||
slug.trim_matches(|c| c == '-').to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
|
||||
@@ -79,6 +97,10 @@ enum OpenedModelHandle<E: Entity> {
|
||||
}
|
||||
|
||||
impl ChannelStore {
|
||||
pub fn global(cx: &AppContext) -> ModelHandle<Self> {
|
||||
cx.global::<ModelHandle<Self>>().clone()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
@@ -92,12 +114,21 @@ impl ChannelStore {
|
||||
let watch_connection_status = cx.spawn_weak(|this, mut cx| async move {
|
||||
while let Some(status) = connection_status.next().await {
|
||||
let this = this.upgrade(&cx)?;
|
||||
match status {
|
||||
client::Status::Connected { .. } => {
|
||||
this.update(&mut cx, |this, cx| this.handle_connect(cx))
|
||||
.await
|
||||
.log_err()?;
|
||||
}
|
||||
client::Status::SignedOut | client::Status::UpgradeRequired => {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(false, cx));
|
||||
}
|
||||
_ => {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(true, cx));
|
||||
}
|
||||
}
|
||||
if status.is_connected() {
|
||||
this.update(&mut cx, |this, cx| this.handle_connect(cx))
|
||||
.await
|
||||
.log_err()?;
|
||||
} else {
|
||||
this.update(&mut cx, |this, cx| this.handle_disconnect(cx));
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
@@ -208,14 +239,73 @@ impl ChannelStore {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<ChannelBuffer>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_buffers,
|
||||
|channel, cx| ChannelBuffer::new(channel, client, cx),
|
||||
|channel, cx| ChannelBuffer::new(channel, client, user_store, cx),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn has_channel_buffer_changed(&self, channel_id: ChannelId) -> Option<bool> {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
.get(&channel_id)
|
||||
.map(|channel| channel.unseen_note_version.is_some())
|
||||
}
|
||||
|
||||
pub fn has_new_messages(&self, channel_id: ChannelId) -> Option<bool> {
|
||||
self.channel_index
|
||||
.by_id()
|
||||
.get(&channel_id)
|
||||
.map(|channel| channel.unseen_message_id.is_some())
|
||||
}
|
||||
|
||||
pub fn notes_changed(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index.note_changed(channel_id, epoch, version);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn new_message(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index.new_message(channel_id, message_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn acknowledge_message_id(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
message_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index
|
||||
.acknowledge_message_id(channel_id, message_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn acknowledge_notes_version(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
self.channel_index
|
||||
.acknowledge_note_version(channel_id, epoch, version);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn open_channel_chat(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
@@ -223,10 +313,11 @@ impl ChannelStore {
|
||||
) -> Task<Result<ModelHandle<ChannelChat>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let this = cx.handle();
|
||||
self.open_channel_resource(
|
||||
channel_id,
|
||||
|this| &mut this.opened_chats,
|
||||
|channel, cx| ChannelChat::new(channel, user_store, client, cx),
|
||||
|channel, cx| ChannelChat::new(channel, this, user_store, client, cx),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
@@ -741,7 +832,7 @@ impl ChannelStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_disconnect(&mut self, cx: &mut ModelContext<Self>) {
|
||||
fn handle_disconnect(&mut self, wait_for_reconnect: bool, cx: &mut ModelContext<Self>) {
|
||||
self.channel_index.clear();
|
||||
self.channel_invitations.clear();
|
||||
self.channel_participants.clear();
|
||||
@@ -752,7 +843,10 @@ impl ChannelStore {
|
||||
|
||||
self.disconnect_channel_buffers_task.get_or_insert_with(|| {
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
cx.background().timer(RECONNECT_TIMEOUT).await;
|
||||
if wait_for_reconnect {
|
||||
cx.background().timer(RECONNECT_TIMEOUT).await;
|
||||
}
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
for (_, buffer) in this.opened_buffers.drain() {
|
||||
@@ -788,6 +882,8 @@ impl ChannelStore {
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
}),
|
||||
),
|
||||
}
|
||||
@@ -796,7 +892,9 @@ impl ChannelStore {
|
||||
let channels_changed = !payload.channels.is_empty()
|
||||
|| !payload.delete_channels.is_empty()
|
||||
|| !payload.insert_edge.is_empty()
|
||||
|| !payload.delete_edge.is_empty();
|
||||
|| !payload.delete_edge.is_empty()
|
||||
|| !payload.unseen_channel_messages.is_empty()
|
||||
|| !payload.unseen_channel_buffer_changes.is_empty();
|
||||
|
||||
if channels_changed {
|
||||
if !payload.delete_channels.is_empty() {
|
||||
@@ -823,6 +921,22 @@ impl ChannelStore {
|
||||
index.insert(channel)
|
||||
}
|
||||
|
||||
for unseen_buffer_change in payload.unseen_channel_buffer_changes {
|
||||
let version = language::proto::deserialize_version(&unseen_buffer_change.version);
|
||||
index.note_changed(
|
||||
unseen_buffer_change.channel_id,
|
||||
unseen_buffer_change.epoch,
|
||||
&version,
|
||||
);
|
||||
}
|
||||
|
||||
for unseen_channel_message in payload.unseen_channel_messages {
|
||||
index.new_messages(
|
||||
unseen_channel_message.channel_id,
|
||||
unseen_channel_message.message_id,
|
||||
);
|
||||
}
|
||||
|
||||
for edge in payload.insert_edge {
|
||||
index.insert_edge(edge.channel_id, edge.parent_id);
|
||||
}
|
||||
@@ -910,12 +1024,6 @@ impl ChannelPath {
|
||||
pub fn channel_id(&self) -> ChannelId {
|
||||
self.0[self.0.len() - 1]
|
||||
}
|
||||
|
||||
pub fn unique_id(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.0.deref().hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChannelPath> for Cow<'static, ChannelPath> {
|
||||
|
||||
@@ -38,6 +38,43 @@ impl ChannelIndex {
|
||||
channels_by_id: &mut self.channels_by_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_note_version(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
) {
|
||||
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
|
||||
let channel = Arc::make_mut(channel);
|
||||
if let Some((unseen_epoch, unseen_version)) = &channel.unseen_note_version {
|
||||
if epoch > *unseen_epoch
|
||||
|| epoch == *unseen_epoch && version.observed_all(unseen_version)
|
||||
{
|
||||
channel.unseen_note_version = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn acknowledge_message_id(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
if let Some(channel) = self.channels_by_id.get_mut(&channel_id) {
|
||||
let channel = Arc::make_mut(channel);
|
||||
if let Some(unseen_message_id) = channel.unseen_message_id {
|
||||
if message_id >= unseen_message_id {
|
||||
channel.unseen_message_id = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
|
||||
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, version);
|
||||
}
|
||||
|
||||
pub fn new_message(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ChannelIndex {
|
||||
@@ -76,6 +113,14 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn note_changed(&mut self, channel_id: ChannelId, epoch: u64, version: &clock::Global) {
|
||||
insert_note_changed(&mut self.channels_by_id, channel_id, epoch, &version);
|
||||
}
|
||||
|
||||
pub fn new_messages(&mut self, channel_id: ChannelId, message_id: u64) {
|
||||
insert_new_message(&mut self.channels_by_id, channel_id, message_id)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, channel_proto: proto::Channel) {
|
||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||
Arc::make_mut(existing_channel).name = channel_proto.name;
|
||||
@@ -85,6 +130,8 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
Arc::new(Channel {
|
||||
id: channel_proto.id,
|
||||
name: channel_proto.name,
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
}),
|
||||
);
|
||||
self.insert_root(channel_proto.id);
|
||||
@@ -160,3 +207,32 @@ fn channel_path_sorting_key<'a>(
|
||||
path.iter()
|
||||
.map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
||||
}
|
||||
|
||||
fn insert_note_changed(
|
||||
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
|
||||
channel_id: u64,
|
||||
epoch: u64,
|
||||
version: &clock::Global,
|
||||
) {
|
||||
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
|
||||
let unseen_version = Arc::make_mut(channel)
|
||||
.unseen_note_version
|
||||
.get_or_insert((0, clock::Global::new()));
|
||||
if epoch > unseen_version.0 {
|
||||
*unseen_version = (epoch, version.clone());
|
||||
} else {
|
||||
unseen_version.1.join(&version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_new_message(
|
||||
channels_by_id: &mut BTreeMap<ChannelId, Arc<Channel>>,
|
||||
channel_id: u64,
|
||||
message_id: u64,
|
||||
) {
|
||||
if let Some(channel) = channels_by_id.get_mut(&channel_id) {
|
||||
let unseen_message_id = Arc::make_mut(channel).unseen_message_id.get_or_insert(0);
|
||||
*unseen_message_id = message_id.max(*unseen_message_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,10 +340,10 @@ fn init_test(cx: &mut AppContext) -> ModelHandle<ChannelStore> {
|
||||
|
||||
cx.foreground().forbid_parking();
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
crate::init(&client);
|
||||
client::init(&client, cx);
|
||||
crate::init(&client, user_store, cx);
|
||||
|
||||
cx.add_model(|cx| ChannelStore::new(client, user_store, cx))
|
||||
ChannelStore::global(cx)
|
||||
}
|
||||
|
||||
fn update_channels(
|
||||
|
||||
@@ -182,6 +182,7 @@ impl Bundle {
|
||||
kCFStringEncodingUTF8,
|
||||
ptr::null(),
|
||||
));
|
||||
// equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
|
||||
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
|
||||
LSOpenFromURLSpec(
|
||||
&LSLaunchURLSpec {
|
||||
|
||||
@@ -33,15 +33,16 @@ parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
tempfile = "3"
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
uuid.workspace = true
|
||||
url = "2.2"
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
|
||||
@@ -34,7 +34,7 @@ use std::{
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Weak},
|
||||
sync::{atomic::AtomicU64, Arc, Weak},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use telemetry::Telemetry;
|
||||
@@ -62,13 +62,15 @@ lazy_static! {
|
||||
.and_then(|v| v.parse().ok());
|
||||
pub static ref ZED_APP_PATH: Option<PathBuf> =
|
||||
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
||||
pub static ref ZED_ALWAYS_ACTIVE: bool =
|
||||
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| e.len() > 0);
|
||||
}
|
||||
|
||||
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!(client, [SignIn, SignOut]);
|
||||
actions!(client, [SignIn, SignOut, Reconnect]);
|
||||
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
settings::register::<TelemetrySettings>(cx);
|
||||
@@ -100,10 +102,21 @@ pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let client = client.clone();
|
||||
move |_: &Reconnect, cx| {
|
||||
if let Some(client) = client.upgrade() {
|
||||
cx.spawn(|cx| async move {
|
||||
client.reconnect(&cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
id: usize,
|
||||
id: AtomicU64,
|
||||
peer: Arc<Peer>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
@@ -372,7 +385,7 @@ impl settings::Setting for TelemetrySettings {
|
||||
impl Client {
|
||||
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
id: 0,
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
telemetry: Telemetry::new(http.clone(), cx),
|
||||
http,
|
||||
@@ -385,17 +398,16 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> usize {
|
||||
self.id
|
||||
pub fn id(&self) -> u64 {
|
||||
self.id.load(std::sync::atomic::Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn http_client(&self) -> Arc<dyn HttpClient> {
|
||||
self.http.clone()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_id(&mut self, id: usize) -> &Self {
|
||||
self.id = id;
|
||||
pub fn set_id(&self, id: u64) -> &Self {
|
||||
self.id.store(id, std::sync::atomic::Ordering::SeqCst);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -452,7 +464,7 @@ impl Client {
|
||||
}
|
||||
|
||||
fn set_status(self: &Arc<Self>, status: Status, cx: &AsyncAppContext) {
|
||||
log::info!("set status on client {}: {:?}", self.id, status);
|
||||
log::info!("set status on client {}: {:?}", self.id(), status);
|
||||
let mut state = self.state.write();
|
||||
*state.status.0.borrow_mut() = status;
|
||||
|
||||
@@ -803,6 +815,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
let credentials = credentials.unwrap();
|
||||
self.set_id(credentials.user_id);
|
||||
|
||||
if was_disconnected {
|
||||
self.set_status(Status::Connecting, cx);
|
||||
@@ -1210,6 +1223,11 @@ impl Client {
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
}
|
||||
|
||||
pub fn reconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||
self.peer.teardown();
|
||||
self.set_status(Status::ConnectionLost, cx);
|
||||
}
|
||||
|
||||
fn connection_id(&self) -> Result<ConnectionId> {
|
||||
if let Status::Connected { connection_id, .. } = *self.status().borrow() {
|
||||
Ok(connection_id)
|
||||
@@ -1219,7 +1237,7 @@ impl Client {
|
||||
}
|
||||
|
||||
pub fn send<T: EnvelopedMessage>(&self, message: T) -> Result<()> {
|
||||
log::debug!("rpc send. client_id:{}, name:{}", self.id, T::NAME);
|
||||
log::debug!("rpc send. client_id:{}, name:{}", self.id(), T::NAME);
|
||||
self.peer.send(self.connection_id()?, message)
|
||||
}
|
||||
|
||||
@@ -1235,7 +1253,7 @@ impl Client {
|
||||
&self,
|
||||
request: T,
|
||||
) -> impl Future<Output = Result<TypedEnvelope<T::Response>>> {
|
||||
let client_id = self.id;
|
||||
let client_id = self.id();
|
||||
log::debug!(
|
||||
"rpc request start. client_id:{}. name:{}",
|
||||
client_id,
|
||||
@@ -1256,7 +1274,7 @@ impl Client {
|
||||
}
|
||||
|
||||
fn respond<T: RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) -> Result<()> {
|
||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
|
||||
self.peer.respond(receipt, response)
|
||||
}
|
||||
|
||||
@@ -1265,7 +1283,7 @@ impl Client {
|
||||
receipt: Receipt<T>,
|
||||
error: proto::Error,
|
||||
) -> Result<()> {
|
||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id, T::NAME);
|
||||
log::debug!("rpc respond. client_id:{}. name:{}", self.id(), T::NAME);
|
||||
self.peer.respond_with_error(receipt, error)
|
||||
}
|
||||
|
||||
@@ -1334,7 +1352,7 @@ impl Client {
|
||||
|
||||
if let Some(handler) = handler {
|
||||
let future = handler(subscriber, message, &self, cx.clone());
|
||||
let client_id = self.id;
|
||||
let client_id = self.id();
|
||||
log::debug!(
|
||||
"rpc message received. client_id:{}, sender_id:{:?}, type:{}",
|
||||
client_id,
|
||||
|
||||
@@ -4,6 +4,7 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::http::HttpClient;
|
||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||
@@ -17,7 +18,8 @@ pub struct Telemetry {
|
||||
#[derive(Default)]
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
|
||||
session_id: Option<Arc<str>>, // Per app launch
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
os_name: &'static str,
|
||||
@@ -40,6 +42,7 @@ lazy_static! {
|
||||
struct ClickhouseEventRequestBody {
|
||||
token: &'static str,
|
||||
installation_id: Option<Arc<str>>,
|
||||
session_id: Option<Arc<str>>,
|
||||
is_staff: Option<bool>,
|
||||
app_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
@@ -88,6 +91,14 @@ pub enum ClickhouseEvent {
|
||||
kind: AssistantKind,
|
||||
model: &'static str,
|
||||
},
|
||||
Cpu {
|
||||
usage_as_percentage: f32,
|
||||
core_count: u32,
|
||||
},
|
||||
Memory {
|
||||
memory_in_bytes: u64,
|
||||
virtual_memory_in_bytes: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -122,6 +133,7 @@ impl Telemetry {
|
||||
release_channel,
|
||||
installation_id: None,
|
||||
metrics_id: None,
|
||||
session_id: None,
|
||||
clickhouse_events_queue: Default::default(),
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
log_file: None,
|
||||
@@ -136,15 +148,61 @@ impl Telemetry {
|
||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||
}
|
||||
|
||||
pub fn start(self: &Arc<Self>, installation_id: Option<String>) {
|
||||
pub fn start(
|
||||
self: &Arc<Self>,
|
||||
installation_id: Option<String>,
|
||||
session_id: String,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let mut state = self.state.lock();
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id.into());
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
if has_clickhouse_events {
|
||||
self.flush_clickhouse_events();
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut system = System::new_all();
|
||||
system.refresh_all();
|
||||
|
||||
loop {
|
||||
// Waiting some amount of time before the first query is important to get a reasonable value
|
||||
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
|
||||
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
|
||||
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
|
||||
|
||||
system.refresh_memory();
|
||||
system.refresh_processes();
|
||||
|
||||
let current_process = Pid::from_u32(std::process::id());
|
||||
let Some(process) = system.processes().get(¤t_process) else {
|
||||
let process = current_process;
|
||||
log::error!("Failed to find own process {process:?} in system process table");
|
||||
// TODO: Fire an error telemetry event
|
||||
return;
|
||||
};
|
||||
|
||||
let memory_event = ClickhouseEvent::Memory {
|
||||
memory_in_bytes: process.memory(),
|
||||
virtual_memory_in_bytes: process.virtual_memory(),
|
||||
};
|
||||
|
||||
let cpu_event = ClickhouseEvent::Cpu {
|
||||
usage_as_percentage: process.cpu_usage(),
|
||||
core_count: system.cpus().len() as u32,
|
||||
};
|
||||
|
||||
let telemetry_settings = cx.update(|cx| *settings::get::<TelemetrySettings>(cx));
|
||||
|
||||
this.report_clickhouse_event(memory_event, telemetry_settings);
|
||||
this.report_clickhouse_event(cpu_event, telemetry_settings);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn set_authenticated_user_info(
|
||||
@@ -230,22 +288,21 @@ impl Telemetry {
|
||||
|
||||
{
|
||||
let state = this.state.lock();
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(
|
||||
&mut json_bytes,
|
||||
&ClickhouseEventRequestBody {
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
installation_id: state.installation_id.clone(),
|
||||
is_staff: state.is_staff.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture,
|
||||
let request_body = ClickhouseEventRequestBody {
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
installation_id: state.installation_id.clone(),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture,
|
||||
|
||||
release_channel: state.release_channel,
|
||||
events,
|
||||
},
|
||||
)?;
|
||||
release_channel: state.release_channel,
|
||||
events,
|
||||
};
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||
}
|
||||
|
||||
this.http_client
|
||||
|
||||
@@ -7,11 +7,15 @@ use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, 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;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParticipantIndex(pub u32);
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
@@ -19,6 +23,13 @@ pub struct User {
|
||||
pub avatar: Option<Arc<ImageData>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Collaborator {
|
||||
pub peer_id: proto::PeerId,
|
||||
pub replica_id: ReplicaId,
|
||||
pub user_id: UserId,
|
||||
}
|
||||
|
||||
impl PartialOrd for User {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
@@ -56,6 +67,7 @@ pub enum ContactRequestStatus {
|
||||
|
||||
pub struct UserStore {
|
||||
users: HashMap<u64, Arc<User>>,
|
||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||
contacts: Vec<Arc<Contact>>,
|
||||
@@ -81,6 +93,7 @@ pub enum Event {
|
||||
kind: ContactEventKind,
|
||||
},
|
||||
ShowContacts,
|
||||
ParticipantIndicesChanged,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -118,6 +131,7 @@ impl UserStore {
|
||||
current_user: current_user_rx,
|
||||
contacts: Default::default(),
|
||||
incoming_contact_requests: Default::default(),
|
||||
participant_indices: Default::default(),
|
||||
outgoing_contact_requests: Default::default(),
|
||||
invite_info: None,
|
||||
client: Arc::downgrade(&client),
|
||||
@@ -581,6 +595,10 @@ impl UserStore {
|
||||
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
||||
}
|
||||
|
||||
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
|
||||
self.users.get(&user_id).cloned()
|
||||
}
|
||||
|
||||
pub fn get_user(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
@@ -641,6 +659,21 @@ impl UserStore {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_participant_indices(
|
||||
&mut self,
|
||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if participant_indices != self.participant_indices {
|
||||
self.participant_indices = participant_indices;
|
||||
cx.emit(Event::ParticipantIndicesChanged);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
|
||||
&self.participant_indices
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
@@ -672,6 +705,16 @@ impl Contact {
|
||||
}
|
||||
}
|
||||
|
||||
impl Collaborator {
|
||||
pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
|
||||
Ok(Self {
|
||||
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
user_id: message.user_id as UserId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||
let mut response = http
|
||||
.get(url, Default::default(), true)
|
||||
|
||||
52
crates/client2/Cargo.toml
Normal file
52
crates/client2/Cargo.toml
Normal file
@@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "client2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/client2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["collections/test-support", "gpui2/test-support", "rpc/test-support"]
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
db = { path = "../db" }
|
||||
gpui2 = { path = "../gpui2" }
|
||||
util = { path = "../util" }
|
||||
rpc = { path = "../rpc" }
|
||||
text = { path = "../text" }
|
||||
settings2 = { path = "../settings2" }
|
||||
feature_flags2 = { path = "../feature_flags2" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
|
||||
anyhow.workspace = true
|
||||
async-recursion = "0.3"
|
||||
async-tungstenite = { version = "0.16", features = ["async-tls"] }
|
||||
futures.workspace = true
|
||||
image = "0.23"
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
tempfile = "3"
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
uuid.workspace = true
|
||||
url = "2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui2 = { path = "../gpui2", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
1665
crates/client2/src/client2.rs
Normal file
1665
crates/client2/src/client2.rs
Normal file
File diff suppressed because it is too large
Load Diff
323
crates/client2/src/telemetry.rs
Normal file
323
crates/client2/src/telemetry.rs
Normal file
@@ -0,0 +1,323 @@
|
||||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use gpui2::{serde_json, AppContext, AppMetadata, Executor, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration};
|
||||
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
||||
use tempfile::NamedTempFile;
|
||||
use util::http::HttpClient;
|
||||
use util::{channel::ReleaseChannel, TryFutureExt};
|
||||
|
||||
pub struct Telemetry {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
executor: Executor,
|
||||
state: Mutex<TelemetryState>,
|
||||
}
|
||||
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation (different for dev, preview, and stable)
|
||||
session_id: Option<Arc<str>>, // Per app launch
|
||||
release_channel: Option<&'static str>,
|
||||
app_metadata: AppMetadata,
|
||||
architecture: &'static str,
|
||||
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
|
||||
flush_clickhouse_events_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
}
|
||||
|
||||
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
|
||||
|
||||
lazy_static! {
|
||||
static ref CLICKHOUSE_EVENTS_URL: String =
|
||||
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ClickhouseEventRequestBody {
|
||||
token: &'static str,
|
||||
installation_id: Option<Arc<str>>,
|
||||
session_id: Option<Arc<str>>,
|
||||
is_staff: Option<bool>,
|
||||
app_version: Option<String>,
|
||||
os_name: &'static str,
|
||||
os_version: Option<String>,
|
||||
architecture: &'static str,
|
||||
release_channel: Option<&'static str>,
|
||||
events: Vec<ClickhouseEventWrapper>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ClickhouseEventWrapper {
|
||||
signed_in: bool,
|
||||
#[serde(flatten)]
|
||||
event: ClickhouseEvent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantKind {
|
||||
Panel,
|
||||
Inline,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClickhouseEvent {
|
||||
Editor {
|
||||
operation: &'static str,
|
||||
file_extension: Option<String>,
|
||||
vim_mode: bool,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
},
|
||||
Copilot {
|
||||
suggestion_id: Option<String>,
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
},
|
||||
Call {
|
||||
operation: &'static str,
|
||||
room_id: Option<u64>,
|
||||
channel_id: Option<u64>,
|
||||
},
|
||||
Assistant {
|
||||
conversation_id: Option<String>,
|
||||
kind: AssistantKind,
|
||||
model: &'static str,
|
||||
},
|
||||
Cpu {
|
||||
usage_as_percentage: f32,
|
||||
core_count: u32,
|
||||
},
|
||||
Memory {
|
||||
memory_in_bytes: u64,
|
||||
virtual_memory_in_bytes: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const MAX_QUEUE_LEN: usize = 1;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const MAX_QUEUE_LEN: usize = 10;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||
let release_channel = if cx.has_global::<ReleaseChannel>() {
|
||||
Some(cx.global::<ReleaseChannel>().display_name())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
http_client: client,
|
||||
executor: cx.executor().clone(),
|
||||
state: Mutex::new(TelemetryState {
|
||||
app_metadata: cx.app_metadata(),
|
||||
architecture: env::consts::ARCH,
|
||||
release_channel,
|
||||
installation_id: None,
|
||||
metrics_id: None,
|
||||
session_id: None,
|
||||
clickhouse_events_queue: Default::default(),
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
}),
|
||||
});
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn log_file_path(&self) -> Option<PathBuf> {
|
||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||
}
|
||||
|
||||
pub fn start(
|
||||
self: &Arc<Self>,
|
||||
installation_id: Option<String>,
|
||||
session_id: String,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let mut state = self.state.lock();
|
||||
state.installation_id = installation_id.map(|id| id.into());
|
||||
state.session_id = Some(session_id.into());
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
if has_clickhouse_events {
|
||||
self.flush_clickhouse_events();
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let mut system = System::new_all();
|
||||
system.refresh_all();
|
||||
|
||||
loop {
|
||||
// Waiting some amount of time before the first query is important to get a reasonable value
|
||||
// https://docs.rs/sysinfo/0.29.10/sysinfo/trait.ProcessExt.html#tymethod.cpu_usage
|
||||
const DURATION_BETWEEN_SYSTEM_EVENTS: Duration = Duration::from_secs(60);
|
||||
smol::Timer::after(DURATION_BETWEEN_SYSTEM_EVENTS).await;
|
||||
|
||||
system.refresh_memory();
|
||||
system.refresh_processes();
|
||||
|
||||
let current_process = Pid::from_u32(std::process::id());
|
||||
let Some(process) = system.processes().get(¤t_process) else {
|
||||
let process = current_process;
|
||||
log::error!("Failed to find own process {process:?} in system process table");
|
||||
// TODO: Fire an error telemetry event
|
||||
return;
|
||||
};
|
||||
|
||||
let memory_event = ClickhouseEvent::Memory {
|
||||
memory_in_bytes: process.memory(),
|
||||
virtual_memory_in_bytes: process.virtual_memory(),
|
||||
};
|
||||
|
||||
let cpu_event = ClickhouseEvent::Cpu {
|
||||
usage_as_percentage: process.cpu_usage(),
|
||||
core_count: system.cpus().len() as u32,
|
||||
};
|
||||
|
||||
let telemetry_settings = if let Ok(telemetry_settings) =
|
||||
cx.update(|cx| *settings2::get::<TelemetrySettings>(cx))
|
||||
{
|
||||
telemetry_settings
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
this.report_clickhouse_event(memory_event, telemetry_settings);
|
||||
this.report_clickhouse_event(cpu_event, telemetry_settings);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn set_authenticated_user_info(
|
||||
self: &Arc<Self>,
|
||||
metrics_id: Option<String>,
|
||||
is_staff: bool,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
if !settings2::get::<TelemetrySettings>(cx).metrics {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
state.is_staff = Some(is_staff);
|
||||
drop(state);
|
||||
}
|
||||
|
||||
pub fn report_clickhouse_event(
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let signed_in = state.metrics_id.is_some();
|
||||
state
|
||||
.clickhouse_events_queue
|
||||
.push(ClickhouseEventWrapper { signed_in, event });
|
||||
|
||||
if state.installation_id.is_some() {
|
||||
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
let this = self.clone();
|
||||
let executor = self.executor.clone();
|
||||
state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||
this.flush_clickhouse_events();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().metrics_id.clone()
|
||||
}
|
||||
|
||||
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().installation_id.clone()
|
||||
}
|
||||
|
||||
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
|
||||
self.state.lock().is_staff
|
||||
}
|
||||
|
||||
fn flush_clickhouse_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.clickhouse_events_queue);
|
||||
state.flush_clickhouse_events_task.take();
|
||||
drop(state);
|
||||
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let mut json_bytes = Vec::new();
|
||||
|
||||
if let Some(file) = &mut this.state.lock().log_file {
|
||||
let file = file.as_file_mut();
|
||||
for event in &mut events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write(b"\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let state = this.state.lock();
|
||||
let request_body = ClickhouseEventRequestBody {
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
installation_id: state.installation_id.clone(),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff.clone(),
|
||||
app_version: state
|
||||
.app_metadata
|
||||
.app_version
|
||||
.map(|version| version.to_string()),
|
||||
os_name: state.app_metadata.os_name,
|
||||
os_version: state
|
||||
.app_metadata
|
||||
.os_version
|
||||
.map(|version| version.to_string()),
|
||||
architecture: state.architecture,
|
||||
|
||||
release_channel: state.release_channel,
|
||||
events,
|
||||
};
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, &request_body)?;
|
||||
}
|
||||
|
||||
this.http_client
|
||||
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
215
crates/client2/src/test.rs
Normal file
215
crates/client2/src/test.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
// use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
||||
// use anyhow::{anyhow, Result};
|
||||
// use futures::{stream::BoxStream, StreamExt};
|
||||
// use gpui2::{Executor, Handle, TestAppContext};
|
||||
// use parking_lot::Mutex;
|
||||
// use rpc::{
|
||||
// proto::{self, GetPrivateUserInfo, GetPrivateUserInfoResponse},
|
||||
// ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||
// };
|
||||
// use std::{rc::Rc, sync::Arc};
|
||||
// use util::http::FakeHttpClient;
|
||||
|
||||
// pub struct FakeServer {
|
||||
// peer: Arc<Peer>,
|
||||
// state: Arc<Mutex<FakeServerState>>,
|
||||
// user_id: u64,
|
||||
// executor: Executor,
|
||||
// }
|
||||
|
||||
// #[derive(Default)]
|
||||
// struct FakeServerState {
|
||||
// incoming: Option<BoxStream<'static, Box<dyn proto::AnyTypedEnvelope>>>,
|
||||
// connection_id: Option<ConnectionId>,
|
||||
// forbid_connections: bool,
|
||||
// auth_count: usize,
|
||||
// access_token: usize,
|
||||
// }
|
||||
|
||||
// impl FakeServer {
|
||||
// pub async fn for_client(
|
||||
// client_user_id: u64,
|
||||
// client: &Arc<Client>,
|
||||
// cx: &TestAppContext,
|
||||
// ) -> Self {
|
||||
// let server = Self {
|
||||
// peer: Peer::new(0),
|
||||
// state: Default::default(),
|
||||
// user_id: client_user_id,
|
||||
// executor: cx.foreground(),
|
||||
// };
|
||||
|
||||
// client
|
||||
// .override_authenticate({
|
||||
// let state = Arc::downgrade(&server.state);
|
||||
// move |cx| {
|
||||
// let state = state.clone();
|
||||
// cx.spawn(move |_| async move {
|
||||
// let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
|
||||
// let mut state = state.lock();
|
||||
// state.auth_count += 1;
|
||||
// let access_token = state.access_token.to_string();
|
||||
// Ok(Credentials {
|
||||
// user_id: client_user_id,
|
||||
// access_token,
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// .override_establish_connection({
|
||||
// let peer = Arc::downgrade(&server.peer);
|
||||
// let state = Arc::downgrade(&server.state);
|
||||
// move |credentials, cx| {
|
||||
// let peer = peer.clone();
|
||||
// let state = state.clone();
|
||||
// let credentials = credentials.clone();
|
||||
// cx.spawn(move |cx| async move {
|
||||
// let state = state.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
|
||||
// let peer = peer.upgrade().ok_or_else(|| anyhow!("server dropped"))?;
|
||||
// if state.lock().forbid_connections {
|
||||
// Err(EstablishConnectionError::Other(anyhow!(
|
||||
// "server is forbidding connections"
|
||||
// )))?
|
||||
// }
|
||||
|
||||
// assert_eq!(credentials.user_id, client_user_id);
|
||||
|
||||
// if credentials.access_token != state.lock().access_token.to_string() {
|
||||
// Err(EstablishConnectionError::Unauthorized)?
|
||||
// }
|
||||
|
||||
// let (client_conn, server_conn, _) = Connection::in_memory(cx.background());
|
||||
// let (connection_id, io, incoming) =
|
||||
// peer.add_test_connection(server_conn, cx.background());
|
||||
// cx.background().spawn(io).detach();
|
||||
// {
|
||||
// let mut state = state.lock();
|
||||
// state.connection_id = Some(connection_id);
|
||||
// state.incoming = Some(incoming);
|
||||
// }
|
||||
// peer.send(
|
||||
// connection_id,
|
||||
// proto::Hello {
|
||||
// peer_id: Some(connection_id.into()),
|
||||
// },
|
||||
// )
|
||||
// .unwrap();
|
||||
|
||||
// Ok(client_conn)
|
||||
// })
|
||||
// }
|
||||
// });
|
||||
|
||||
// client
|
||||
// .authenticate_and_connect(false, &cx.to_async())
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// server
|
||||
// }
|
||||
|
||||
// pub fn disconnect(&self) {
|
||||
// if self.state.lock().connection_id.is_some() {
|
||||
// self.peer.disconnect(self.connection_id());
|
||||
// let mut state = self.state.lock();
|
||||
// state.connection_id.take();
|
||||
// state.incoming.take();
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn auth_count(&self) -> usize {
|
||||
// self.state.lock().auth_count
|
||||
// }
|
||||
|
||||
// pub fn roll_access_token(&self) {
|
||||
// self.state.lock().access_token += 1;
|
||||
// }
|
||||
|
||||
// pub fn forbid_connections(&self) {
|
||||
// self.state.lock().forbid_connections = true;
|
||||
// }
|
||||
|
||||
// pub fn allow_connections(&self) {
|
||||
// self.state.lock().forbid_connections = false;
|
||||
// }
|
||||
|
||||
// pub fn send<T: proto::EnvelopedMessage>(&self, message: T) {
|
||||
// self.peer.send(self.connection_id(), message).unwrap();
|
||||
// }
|
||||
|
||||
// #[allow(clippy::await_holding_lock)]
|
||||
// pub async fn receive<M: proto::EnvelopedMessage>(&self) -> Result<TypedEnvelope<M>> {
|
||||
// self.executor.start_waiting();
|
||||
|
||||
// loop {
|
||||
// let message = self
|
||||
// .state
|
||||
// .lock()
|
||||
// .incoming
|
||||
// .as_mut()
|
||||
// .expect("not connected")
|
||||
// .next()
|
||||
// .await
|
||||
// .ok_or_else(|| anyhow!("other half hung up"))?;
|
||||
// self.executor.finish_waiting();
|
||||
// let type_name = message.payload_type_name();
|
||||
// let message = message.into_any();
|
||||
|
||||
// if message.is::<TypedEnvelope<M>>() {
|
||||
// return Ok(*message.downcast().unwrap());
|
||||
// }
|
||||
|
||||
// if message.is::<TypedEnvelope<GetPrivateUserInfo>>() {
|
||||
// self.respond(
|
||||
// message
|
||||
// .downcast::<TypedEnvelope<GetPrivateUserInfo>>()
|
||||
// .unwrap()
|
||||
// .receipt(),
|
||||
// GetPrivateUserInfoResponse {
|
||||
// metrics_id: "the-metrics-id".into(),
|
||||
// staff: false,
|
||||
// flags: Default::default(),
|
||||
// },
|
||||
// );
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// panic!(
|
||||
// "fake server received unexpected message type: {:?}",
|
||||
// type_name
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn respond<T: proto::RequestMessage>(&self, receipt: Receipt<T>, response: T::Response) {
|
||||
// self.peer.respond(receipt, response).unwrap()
|
||||
// }
|
||||
|
||||
// fn connection_id(&self) -> ConnectionId {
|
||||
// self.state.lock().connection_id.expect("not connected")
|
||||
// }
|
||||
|
||||
// pub async fn build_user_store(
|
||||
// &self,
|
||||
// client: Arc<Client>,
|
||||
// cx: &mut TestAppContext,
|
||||
// ) -> ModelHandle<UserStore> {
|
||||
// let http_client = FakeHttpClient::with_404_response();
|
||||
// let user_store = cx.add_model(|cx| UserStore::new(client, http_client, cx));
|
||||
// assert_eq!(
|
||||
// self.receive::<proto::GetUsers>()
|
||||
// .await
|
||||
// .unwrap()
|
||||
// .payload
|
||||
// .user_ids,
|
||||
// &[self.user_id]
|
||||
// );
|
||||
// user_store
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Drop for FakeServer {
|
||||
// fn drop(&mut self) {
|
||||
// self.disconnect();
|
||||
// }
|
||||
// }
|
||||
739
crates/client2/src/user.rs
Normal file
739
crates/client2/src/user.rs
Normal file
@@ -0,0 +1,739 @@
|
||||
use super::{proto, Client, Status, TypedEnvelope};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::{hash_map::Entry, HashMap, HashSet};
|
||||
use feature_flags2::FeatureFlagAppExt;
|
||||
use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
||||
use gpui2::{AsyncAppContext, EventEmitter, Handle, ImageData, ModelContext, 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;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ParticipantIndex(pub u32);
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct User {
|
||||
pub id: UserId,
|
||||
pub github_login: String,
|
||||
pub avatar: Option<Arc<ImageData>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Collaborator {
|
||||
pub peer_id: proto::PeerId,
|
||||
pub replica_id: ReplicaId,
|
||||
pub user_id: UserId,
|
||||
}
|
||||
|
||||
impl PartialOrd for User {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for User {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
self.github_login.cmp(&other.github_login)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for User {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id && self.github_login == other.github_login
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for User {}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Contact {
|
||||
pub user: Arc<User>,
|
||||
pub online: bool,
|
||||
pub busy: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ContactRequestStatus {
|
||||
None,
|
||||
RequestSent,
|
||||
RequestReceived,
|
||||
RequestAccepted,
|
||||
}
|
||||
|
||||
pub struct UserStore {
|
||||
users: HashMap<u64, Arc<User>>,
|
||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||
contacts: Vec<Arc<Contact>>,
|
||||
incoming_contact_requests: Vec<Arc<User>>,
|
||||
outgoing_contact_requests: Vec<Arc<User>>,
|
||||
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<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InviteInfo {
|
||||
pub count: u32,
|
||||
pub url: Arc<str>,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Contact {
|
||||
user: Arc<User>,
|
||||
kind: ContactEventKind,
|
||||
},
|
||||
ShowContacts,
|
||||
ParticipantIndicesChanged,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ContactEventKind {
|
||||
Requested,
|
||||
Accepted,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl EventEmitter for UserStore {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
enum UpdateContacts {
|
||||
Update(proto::UpdateContacts),
|
||||
Wait(postage::barrier::Sender),
|
||||
Clear(postage::barrier::Sender),
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
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![
|
||||
client.add_message_handler(cx.weak_handle(), Self::handle_update_contacts),
|
||||
client.add_message_handler(cx.weak_handle(), Self::handle_update_invite_info),
|
||||
client.add_message_handler(cx.weak_handle(), Self::handle_show_contacts),
|
||||
];
|
||||
Self {
|
||||
users: Default::default(),
|
||||
current_user: current_user_rx,
|
||||
contacts: Default::default(),
|
||||
incoming_contact_requests: Default::default(),
|
||||
participant_indices: Default::default(),
|
||||
outgoing_contact_requests: Default::default(),
|
||||
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 {
|
||||
if let Ok(task) =
|
||||
this.update(&mut cx, |this, cx| this.update_contacts(message, cx))
|
||||
{
|
||||
task.log_err().await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
_maintain_current_user: cx.spawn(|this, mut cx| async move {
|
||||
let mut status = client.status();
|
||||
while let Some(status) = status.next().await {
|
||||
match status {
|
||||
Status::Connected { .. } => {
|
||||
if let Some(user_id) = client.user_id() {
|
||||
let fetch_user = if let Ok(fetch_user) = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.get_user(user_id, cx).log_err()
|
||||
}) {
|
||||
fetch_user
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
let fetch_metrics_id =
|
||||
client.request(proto::GetPrivateUserInfo {}).log_err();
|
||||
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(info) = info {
|
||||
cx.update_flags(info.staff, info.flags);
|
||||
client.telemetry.set_authenticated_user_info(
|
||||
Some(info.metrics_id.clone()),
|
||||
info.staff,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})?;
|
||||
|
||||
current_user_tx.send(user).await.ok();
|
||||
|
||||
this.update(&mut cx, |_, cx| cx.notify())?;
|
||||
}
|
||||
}
|
||||
Status::SignedOut => {
|
||||
current_user_tx.send(None).await.ok();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
Status::ConnectionLost => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
cx.notify();
|
||||
this.clear_contacts()
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}),
|
||||
pending_contact_requests: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.users.clear();
|
||||
}
|
||||
|
||||
async fn handle_update_invite_info(
|
||||
this: Handle<Self>,
|
||||
message: TypedEnvelope<proto::UpdateInviteInfo>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.invite_info = Some(InviteInfo {
|
||||
url: Arc::from(message.payload.url),
|
||||
count: message.payload.count,
|
||||
});
|
||||
cx.notify();
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_show_contacts(
|
||||
this: Handle<Self>,
|
||||
_: TypedEnvelope<proto::ShowContacts>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |_, cx| cx.emit(Event::ShowContacts))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn invite_info(&self) -> Option<&InviteInfo> {
|
||||
self.invite_info.as_ref()
|
||||
}
|
||||
|
||||
async fn handle_update_contacts(
|
||||
this: Handle<Self>,
|
||||
message: TypedEnvelope<proto::UpdateContacts>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.update_contacts_tx
|
||||
.unbounded_send(UpdateContacts::Update(message.payload))
|
||||
.unwrap();
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_contacts(
|
||||
&mut self,
|
||||
message: UpdateContacts,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
match message {
|
||||
UpdateContacts::Wait(barrier) => {
|
||||
drop(barrier);
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
UpdateContacts::Clear(barrier) => {
|
||||
self.contacts.clear();
|
||||
self.incoming_contact_requests.clear();
|
||||
self.outgoing_contact_requests.clear();
|
||||
drop(barrier);
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
UpdateContacts::Update(message) => {
|
||||
let mut user_ids = HashSet::default();
|
||||
for contact in &message.contacts {
|
||||
user_ids.insert(contact.user_id);
|
||||
}
|
||||
user_ids.extend(message.incoming_requests.iter().map(|req| req.requester_id));
|
||||
user_ids.extend(message.outgoing_requests.iter());
|
||||
|
||||
let load_users = self.get_users(user_ids.into_iter().collect(), cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
load_users.await?;
|
||||
|
||||
// Users are fetched in parallel above and cached in call to get_users
|
||||
// No need to paralellize here
|
||||
let mut updated_contacts = Vec::new();
|
||||
let this = this
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("can't upgrade user store handle"))?;
|
||||
for contact in message.contacts {
|
||||
let should_notify = contact.should_notify;
|
||||
updated_contacts.push((
|
||||
Arc::new(Contact::from_proto(contact, &this, &mut cx).await?),
|
||||
should_notify,
|
||||
));
|
||||
}
|
||||
|
||||
let mut incoming_requests = Vec::new();
|
||||
for request in message.incoming_requests {
|
||||
incoming_requests.push({
|
||||
let user = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.get_user(request.requester_id, cx)
|
||||
})?
|
||||
.await?;
|
||||
(user, request.should_notify)
|
||||
});
|
||||
}
|
||||
|
||||
let mut outgoing_requests = Vec::new();
|
||||
for requested_user_id in message.outgoing_requests {
|
||||
outgoing_requests.push(
|
||||
this.update(&mut cx, |this, cx| this.get_user(requested_user_id, cx))?
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
let removed_contacts =
|
||||
HashSet::<u64>::from_iter(message.remove_contacts.iter().copied());
|
||||
let removed_incoming_requests =
|
||||
HashSet::<u64>::from_iter(message.remove_incoming_requests.iter().copied());
|
||||
let removed_outgoing_requests =
|
||||
HashSet::<u64>::from_iter(message.remove_outgoing_requests.iter().copied());
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// Remove contacts
|
||||
this.contacts
|
||||
.retain(|contact| !removed_contacts.contains(&contact.user.id));
|
||||
// Update existing contacts and insert new ones
|
||||
for (updated_contact, should_notify) in updated_contacts {
|
||||
if should_notify {
|
||||
cx.emit(Event::Contact {
|
||||
user: updated_contact.user.clone(),
|
||||
kind: ContactEventKind::Accepted,
|
||||
});
|
||||
}
|
||||
match this.contacts.binary_search_by_key(
|
||||
&&updated_contact.user.github_login,
|
||||
|contact| &contact.user.github_login,
|
||||
) {
|
||||
Ok(ix) => this.contacts[ix] = updated_contact,
|
||||
Err(ix) => this.contacts.insert(ix, updated_contact),
|
||||
}
|
||||
}
|
||||
|
||||
// Remove incoming contact requests
|
||||
this.incoming_contact_requests.retain(|user| {
|
||||
if removed_incoming_requests.contains(&user.id) {
|
||||
cx.emit(Event::Contact {
|
||||
user: user.clone(),
|
||||
kind: ContactEventKind::Cancelled,
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
// Update existing incoming requests and insert new ones
|
||||
for (user, should_notify) in incoming_requests {
|
||||
if should_notify {
|
||||
cx.emit(Event::Contact {
|
||||
user: user.clone(),
|
||||
kind: ContactEventKind::Requested,
|
||||
});
|
||||
}
|
||||
|
||||
match this
|
||||
.incoming_contact_requests
|
||||
.binary_search_by_key(&&user.github_login, |contact| {
|
||||
&contact.github_login
|
||||
}) {
|
||||
Ok(ix) => this.incoming_contact_requests[ix] = user,
|
||||
Err(ix) => this.incoming_contact_requests.insert(ix, user),
|
||||
}
|
||||
}
|
||||
|
||||
// Remove outgoing contact requests
|
||||
this.outgoing_contact_requests
|
||||
.retain(|user| !removed_outgoing_requests.contains(&user.id));
|
||||
// Update existing incoming requests and insert new ones
|
||||
for request in outgoing_requests {
|
||||
match this
|
||||
.outgoing_contact_requests
|
||||
.binary_search_by_key(&&request.github_login, |contact| {
|
||||
&contact.github_login
|
||||
}) {
|
||||
Ok(ix) => this.outgoing_contact_requests[ix] = request,
|
||||
Err(ix) => this.outgoing_contact_requests.insert(ix, request),
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contacts(&self) -> &[Arc<Contact>] {
|
||||
&self.contacts
|
||||
}
|
||||
|
||||
pub fn has_contact(&self, user: &Arc<User>) -> bool {
|
||||
self.contacts
|
||||
.binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn incoming_contact_requests(&self) -> &[Arc<User>] {
|
||||
&self.incoming_contact_requests
|
||||
}
|
||||
|
||||
pub fn outgoing_contact_requests(&self) -> &[Arc<User>] {
|
||||
&self.outgoing_contact_requests
|
||||
}
|
||||
|
||||
pub fn is_contact_request_pending(&self, user: &User) -> bool {
|
||||
self.pending_contact_requests.contains_key(&user.id)
|
||||
}
|
||||
|
||||
pub fn contact_request_status(&self, user: &User) -> ContactRequestStatus {
|
||||
if self
|
||||
.contacts
|
||||
.binary_search_by_key(&&user.github_login, |contact| &contact.user.github_login)
|
||||
.is_ok()
|
||||
{
|
||||
ContactRequestStatus::RequestAccepted
|
||||
} else if self
|
||||
.outgoing_contact_requests
|
||||
.binary_search_by_key(&&user.github_login, |user| &user.github_login)
|
||||
.is_ok()
|
||||
{
|
||||
ContactRequestStatus::RequestSent
|
||||
} else if self
|
||||
.incoming_contact_requests
|
||||
.binary_search_by_key(&&user.github_login, |user| &user.github_login)
|
||||
.is_ok()
|
||||
{
|
||||
ContactRequestStatus::RequestReceived
|
||||
} else {
|
||||
ContactRequestStatus::None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_contact(
|
||||
&mut self,
|
||||
responder_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.perform_contact_request(responder_id, proto::RequestContact { responder_id }, cx)
|
||||
}
|
||||
|
||||
pub fn remove_contact(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.perform_contact_request(user_id, proto::RemoveContact { user_id }, cx)
|
||||
}
|
||||
|
||||
pub fn respond_to_contact_request(
|
||||
&mut self,
|
||||
requester_id: u64,
|
||||
accept: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.perform_contact_request(
|
||||
requester_id,
|
||||
proto::RespondToContactRequest {
|
||||
requester_id,
|
||||
response: if accept {
|
||||
proto::ContactRequestResponse::Accept
|
||||
} else {
|
||||
proto::ContactRequestResponse::Decline
|
||||
} as i32,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn dismiss_contact_request(
|
||||
&mut self,
|
||||
requester_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.upgrade();
|
||||
cx.spawn(move |_, _| async move {
|
||||
client
|
||||
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
|
||||
.request(proto::RespondToContactRequest {
|
||||
requester_id,
|
||||
response: proto::ContactRequestResponse::Dismiss as i32,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn perform_contact_request<T: RequestMessage>(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
request: T,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.upgrade();
|
||||
*self.pending_contact_requests.entry(user_id).or_insert(0) += 1;
|
||||
cx.notify();
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let response = client
|
||||
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
|
||||
.request(request)
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Entry::Occupied(mut request_count) =
|
||||
this.pending_contact_requests.entry(user_id)
|
||||
{
|
||||
*request_count.get_mut() -= 1;
|
||||
if *request_count.get() == 0 {
|
||||
request_count.remove();
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
response?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clear_contacts(&mut self) -> impl Future<Output = ()> {
|
||||
let (tx, mut rx) = postage::barrier::channel();
|
||||
self.update_contacts_tx
|
||||
.unbounded_send(UpdateContacts::Clear(tx))
|
||||
.unwrap();
|
||||
async move {
|
||||
rx.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contact_updates_done(&mut self) -> impl Future<Output = ()> {
|
||||
let (tx, mut rx) = postage::barrier::channel();
|
||||
self.update_contacts_tx
|
||||
.unbounded_send(UpdateContacts::Wait(tx))
|
||||
.unwrap();
|
||||
async move {
|
||||
rx.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_users(
|
||||
&mut self,
|
||||
user_ids: Vec<u64>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
let mut user_ids_to_fetch = user_ids.clone();
|
||||
user_ids_to_fetch.retain(|id| !self.users.contains_key(id));
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if !user_ids_to_fetch.is_empty() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.load_users(
|
||||
proto::GetUsers {
|
||||
user_ids: user_ids_to_fetch,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
user_ids
|
||||
.iter()
|
||||
.map(|user_id| {
|
||||
this.users
|
||||
.get(user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("user {} not found", user_id))
|
||||
})
|
||||
.collect()
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
pub fn fuzzy_search_users(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Vec<Arc<User>>>> {
|
||||
self.load_users(proto::FuzzySearchUsers { query }, cx)
|
||||
}
|
||||
|
||||
pub fn get_cached_user(&self, user_id: u64) -> Option<Arc<User>> {
|
||||
self.users.get(&user_id).cloned()
|
||||
}
|
||||
|
||||
pub fn get_user(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Arc<User>>> {
|
||||
if let Some(user) = self.users.get(&user_id).cloned() {
|
||||
return Task::ready(Ok(user));
|
||||
}
|
||||
|
||||
let load_users = self.get_users(vec![user_id], cx);
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
load_users.await?;
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.users
|
||||
.get(&user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("server responded with no users"))
|
||||
})?
|
||||
})
|
||||
}
|
||||
|
||||
pub fn current_user(&self) -> Option<Arc<User>> {
|
||||
self.current_user.borrow().clone()
|
||||
}
|
||||
|
||||
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
||||
self.current_user.clone()
|
||||
}
|
||||
|
||||
fn load_users(
|
||||
&mut self,
|
||||
request: impl RequestMessage<Response = UsersResponse>,
|
||||
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;
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
for user in &users {
|
||||
this.users.insert(user.id, user.clone());
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(users)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_participant_indices(
|
||||
&mut self,
|
||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if participant_indices != self.participant_indices {
|
||||
self.participant_indices = participant_indices;
|
||||
cx.emit(Event::ParticipantIndicesChanged);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn participant_indices(&self) -> &HashMap<u64, ParticipantIndex> {
|
||||
&self.participant_indices
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
async fn new(message: proto::User, http: &dyn HttpClient) -> Arc<Self> {
|
||||
Arc::new(User {
|
||||
id: message.id,
|
||||
github_login: message.github_login,
|
||||
avatar: fetch_avatar(http, &message.avatar_url).warn_on_err().await,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
async fn from_proto(
|
||||
contact: proto::Contact,
|
||||
user_store: &Handle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let user = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_user(contact.user_id, cx)
|
||||
})?
|
||||
.await?;
|
||||
Ok(Self {
|
||||
user,
|
||||
online: contact.online,
|
||||
busy: contact.busy,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Collaborator {
|
||||
pub fn from_proto(message: proto::Collaborator) -> Result<Self> {
|
||||
Ok(Self {
|
||||
peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?,
|
||||
replica_id: message.replica_id as ReplicaId,
|
||||
user_id: message.user_id as UserId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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.21.0"
|
||||
version = "0.24.0"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
@@ -42,14 +42,12 @@ rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||
scrypt = "0.7"
|
||||
smallvec.workspace = true
|
||||
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
|
||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
|
||||
sea-query = "0.27"
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls", "with-uuid"] }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha-1 = "0.9"
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
time.workspace = true
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = "0.17"
|
||||
@@ -59,6 +57,7 @@ toml.workspace = true
|
||||
tracing = "0.1.34"
|
||||
tracing-log = "0.1.3"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
audio = { path = "../audio" }
|
||||
@@ -73,6 +72,7 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||
git = { path = "../git", features = ["test-support"] }
|
||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
node_runtime = { path = "../node_runtime" }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
@@ -87,9 +87,9 @@ env_logger.workspace = true
|
||||
indoc.workspace = true
|
||||
util = { path = "../util" }
|
||||
lazy_static.workspace = true
|
||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
|
||||
sea-orm = { version = "0.12.x", features = ["sqlx-sqlite"] }
|
||||
serde_json.workspace = true
|
||||
sqlx = { version = "0.6", features = ["sqlite"] }
|
||||
sqlx = { version = "0.7", features = ["sqlite"] }
|
||||
unindent.workspace = true
|
||||
|
||||
[features]
|
||||
|
||||
@@ -37,8 +37,10 @@ CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
|
||||
CREATE TABLE "rooms" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"live_kit_room" VARCHAR NOT NULL,
|
||||
"enviroment" VARCHAR,
|
||||
"channel_id" INTEGER REFERENCES channels (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -158,7 +160,8 @@ CREATE TABLE "room_participants" (
|
||||
"initial_project_id" INTEGER,
|
||||
"calling_user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
"calling_connection_id" INTEGER NOT NULL,
|
||||
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL
|
||||
"calling_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE SET NULL,
|
||||
"participant_index" INTEGER
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_room_participants_on_user_id" ON "room_participants" ("user_id");
|
||||
CREATE INDEX "index_room_participants_on_room_id" ON "room_participants" ("room_id");
|
||||
@@ -288,3 +291,24 @@ CREATE TABLE "user_features" (
|
||||
CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id");
|
||||
CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id");
|
||||
CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id");
|
||||
|
||||
|
||||
CREATE TABLE "observed_buffer_edits" (
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
|
||||
"epoch" INTEGER NOT NULL,
|
||||
"lamport_timestamp" INTEGER NOT NULL,
|
||||
"replica_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, buffer_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_observed_buffers_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "observed_channel_messages" (
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"channel_message_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, channel_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id");
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE IF NOT EXISTS "observed_buffer_edits" (
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE,
|
||||
"epoch" INTEGER NOT NULL,
|
||||
"lamport_timestamp" INTEGER NOT NULL,
|
||||
"replica_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, buffer_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_observed_buffer_user_and_buffer_id" ON "observed_buffer_edits" ("user_id", "buffer_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "observed_channel_messages" (
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||
"channel_message_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY (user_id, channel_id)
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "index_observed_channel_messages_user_and_channel_id" ON "observed_channel_messages" ("user_id", "channel_id");
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE room_participants ADD COLUMN participant_index INTEGER;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE rooms ADD COLUMN enviroment TEXT;
|
||||
@@ -0,0 +1 @@
|
||||
CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
||||
@@ -19,11 +19,12 @@ use rpc::{
|
||||
ConnectionId,
|
||||
};
|
||||
use sea_orm::{
|
||||
entity::prelude::*, ActiveValue, Condition, ConnectionTrait, DatabaseConnection,
|
||||
DatabaseTransaction, DbErr, FromQueryResult, IntoActiveModel, IsolationLevel, JoinType,
|
||||
QueryOrder, QuerySelect, Statement, TransactionTrait,
|
||||
entity::prelude::*,
|
||||
sea_query::{Alias, Expr, OnConflict, Query},
|
||||
ActiveValue, Condition, ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbErr,
|
||||
FromQueryResult, IntoActiveModel, IsolationLevel, JoinType, QueryOrder, QuerySelect, Statement,
|
||||
TransactionTrait,
|
||||
};
|
||||
use sea_query::{Alias, Expr, OnConflict, Query};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{
|
||||
migrate::{Migrate, Migration, MigrationSource},
|
||||
@@ -62,6 +63,7 @@ pub struct Database {
|
||||
// separate files in the `queries` folder.
|
||||
impl Database {
|
||||
pub async fn new(options: ConnectOptions, executor: Executor) -> Result<Self> {
|
||||
sqlx::any::install_default_drivers();
|
||||
Ok(Self {
|
||||
options: options.clone(),
|
||||
pool: sea_orm::Database::connect(options).await?,
|
||||
@@ -119,7 +121,7 @@ impl Database {
|
||||
Ok(new_migrations)
|
||||
}
|
||||
|
||||
async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
|
||||
pub async fn transaction<F, Fut, T>(&self, f: F) -> Result<T>
|
||||
where
|
||||
F: Send + Fn(TransactionHandle) -> Fut,
|
||||
Fut: Send + Future<Output = Result<T>>,
|
||||
@@ -321,7 +323,7 @@ fn is_serialization_error(error: &Error) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
struct TransactionHandle(Arc<Option<DatabaseTransaction>>);
|
||||
pub struct TransactionHandle(Arc<Option<DatabaseTransaction>>);
|
||||
|
||||
impl Deref for TransactionHandle {
|
||||
type Target = DatabaseTransaction;
|
||||
@@ -437,6 +439,8 @@ pub struct ChannelsForUser {
|
||||
pub channels: ChannelGraph,
|
||||
pub channel_participants: HashMap<ChannelId, Vec<UserId>>,
|
||||
pub channels_with_admin_privileges: HashSet<ChannelId>,
|
||||
pub unseen_buffer_changes: Vec<proto::UnseenChannelBufferChange>,
|
||||
pub channel_messages: Vec<proto::UnseenChannelMessage>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -510,7 +514,7 @@ pub struct RefreshedRoom {
|
||||
|
||||
pub struct RefreshedChannelBuffer {
|
||||
pub connection_ids: Vec<ConnectionId>,
|
||||
pub removed_collaborators: Vec<proto::RemoveChannelBufferCollaborator>,
|
||||
pub collaborators: Vec<proto::Collaborator>,
|
||||
}
|
||||
|
||||
pub struct Project {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::Result;
|
||||
use sea_orm::DbErr;
|
||||
use sea_query::{Value, ValueTypeErr};
|
||||
use sea_orm::{entity::prelude::*, DbErr};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
macro_rules! id_type {
|
||||
@@ -17,6 +16,7 @@ macro_rules! id_type {
|
||||
Hash,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
DeriveValueType,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct $name(pub i32);
|
||||
@@ -42,40 +42,6 @@ macro_rules! id_type {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$name> for sea_query::Value {
|
||||
fn from(value: $name) -> Self {
|
||||
sea_query::Value::Int(Some(value.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl sea_orm::TryGetable for $name {
|
||||
fn try_get(
|
||||
res: &sea_orm::QueryResult,
|
||||
pre: &str,
|
||||
col: &str,
|
||||
) -> Result<Self, sea_orm::TryGetError> {
|
||||
Ok(Self(i32::try_get(res, pre, col)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl sea_query::ValueType for $name {
|
||||
fn try_from(v: Value) -> Result<Self, sea_query::ValueTypeErr> {
|
||||
Ok(Self(value_to_integer(v)?))
|
||||
}
|
||||
|
||||
fn type_name() -> String {
|
||||
stringify!($name).into()
|
||||
}
|
||||
|
||||
fn array_type() -> sea_query::ArrayType {
|
||||
sea_query::ArrayType::Int
|
||||
}
|
||||
|
||||
fn column_type() -> sea_query::ColumnType {
|
||||
sea_query::ColumnType::Integer(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl sea_orm::TryFromU64 for $name {
|
||||
fn try_from_u64(n: u64) -> Result<Self, DbErr> {
|
||||
Ok(Self(n.try_into().map_err(|_| {
|
||||
@@ -88,7 +54,7 @@ macro_rules! id_type {
|
||||
}
|
||||
}
|
||||
|
||||
impl sea_query::Nullable for $name {
|
||||
impl sea_orm::sea_query::Nullable for $name {
|
||||
fn null() -> Value {
|
||||
Value::Int(None)
|
||||
}
|
||||
@@ -96,20 +62,6 @@ macro_rules! id_type {
|
||||
};
|
||||
}
|
||||
|
||||
fn value_to_integer(v: Value) -> Result<i32, ValueTypeErr> {
|
||||
match v {
|
||||
Value::TinyInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
|
||||
Value::SmallInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
|
||||
Value::Int(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
|
||||
Value::BigInt(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
|
||||
Value::TinyUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
|
||||
Value::SmallUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
|
||||
Value::Unsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
|
||||
Value::BigUnsigned(Some(int)) => int.try_into().map_err(|_| ValueTypeErr),
|
||||
_ => Err(ValueTypeErr),
|
||||
}
|
||||
}
|
||||
|
||||
id_type!(BufferId);
|
||||
id_type!(AccessTokenId);
|
||||
id_type!(ChannelChatParticipantId);
|
||||
|
||||
@@ -2,6 +2,12 @@ use super::*;
|
||||
use prost::Message;
|
||||
use text::{EditOperation, UndoOperation};
|
||||
|
||||
pub struct LeftChannelBuffer {
|
||||
pub channel_id: ChannelId,
|
||||
pub collaborators: Vec<proto::Collaborator>,
|
||||
pub connections: Vec<ConnectionId>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn join_channel_buffer(
|
||||
&self,
|
||||
@@ -68,7 +74,32 @@ impl Database {
|
||||
.await?;
|
||||
collaborators.push(collaborator);
|
||||
|
||||
let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?;
|
||||
let (base_text, operations, max_operation) =
|
||||
self.get_buffer_state(&buffer, &tx).await?;
|
||||
|
||||
// Save the last observed operation
|
||||
if let Some(op) = max_operation {
|
||||
observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
buffer_id: ActiveValue::Set(buffer.id),
|
||||
epoch: ActiveValue::Set(op.epoch),
|
||||
lamport_timestamp: ActiveValue::Set(op.lamport_timestamp),
|
||||
replica_id: ActiveValue::Set(op.replica_id),
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
observed_buffer_edits::Column::UserId,
|
||||
observed_buffer_edits::Column::BufferId,
|
||||
])
|
||||
.update_columns([
|
||||
observed_buffer_edits::Column::Epoch,
|
||||
observed_buffer_edits::Column::LamportTimestamp,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(proto::JoinChannelBufferResponse {
|
||||
buffer_id: buffer.id.to_proto(),
|
||||
@@ -204,23 +235,26 @@ impl Database {
|
||||
server_id: ServerId,
|
||||
) -> Result<RefreshedChannelBuffer> {
|
||||
self.transaction(|tx| async move {
|
||||
let collaborators = channel_buffer_collaborator::Entity::find()
|
||||
let db_collaborators = channel_buffer_collaborator::Entity::find()
|
||||
.filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut connection_ids = Vec::new();
|
||||
let mut removed_collaborators = Vec::new();
|
||||
let mut collaborators = Vec::new();
|
||||
let mut collaborator_ids_to_remove = Vec::new();
|
||||
for collaborator in &collaborators {
|
||||
if !collaborator.connection_lost && collaborator.connection_server_id == server_id {
|
||||
connection_ids.push(collaborator.connection());
|
||||
for db_collaborator in &db_collaborators {
|
||||
if !db_collaborator.connection_lost
|
||||
&& db_collaborator.connection_server_id == server_id
|
||||
{
|
||||
connection_ids.push(db_collaborator.connection());
|
||||
collaborators.push(proto::Collaborator {
|
||||
peer_id: Some(db_collaborator.connection().into()),
|
||||
replica_id: db_collaborator.replica_id.0 as u32,
|
||||
user_id: db_collaborator.user_id.to_proto(),
|
||||
})
|
||||
} else {
|
||||
removed_collaborators.push(proto::RemoveChannelBufferCollaborator {
|
||||
channel_id: channel_id.to_proto(),
|
||||
peer_id: Some(collaborator.connection().into()),
|
||||
});
|
||||
collaborator_ids_to_remove.push(collaborator.id);
|
||||
collaborator_ids_to_remove.push(db_collaborator.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +265,7 @@ impl Database {
|
||||
|
||||
Ok(RefreshedChannelBuffer {
|
||||
connection_ids,
|
||||
removed_collaborators,
|
||||
collaborators,
|
||||
})
|
||||
})
|
||||
.await
|
||||
@@ -241,7 +275,7 @@ impl Database {
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
connection: ConnectionId,
|
||||
) -> Result<Vec<ConnectionId>> {
|
||||
) -> Result<LeftChannelBuffer> {
|
||||
self.transaction(|tx| async move {
|
||||
self.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
||||
.await
|
||||
@@ -275,7 +309,7 @@ impl Database {
|
||||
pub async fn leave_channel_buffers(
|
||||
&self,
|
||||
connection: ConnectionId,
|
||||
) -> Result<Vec<(ChannelId, Vec<ConnectionId>)>> {
|
||||
) -> Result<Vec<LeftChannelBuffer>> {
|
||||
self.transaction(|tx| async move {
|
||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||
enum QueryChannelIds {
|
||||
@@ -294,10 +328,10 @@ impl Database {
|
||||
|
||||
let mut result = Vec::new();
|
||||
for channel_id in channel_ids {
|
||||
let collaborators = self
|
||||
let left_channel_buffer = self
|
||||
.leave_channel_buffer_internal(channel_id, connection, &*tx)
|
||||
.await?;
|
||||
result.push((channel_id, collaborators));
|
||||
result.push(left_channel_buffer);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
@@ -310,7 +344,7 @@ impl Database {
|
||||
channel_id: ChannelId,
|
||||
connection: ConnectionId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<ConnectionId>> {
|
||||
) -> Result<LeftChannelBuffer> {
|
||||
let result = channel_buffer_collaborator::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
@@ -327,6 +361,7 @@ impl Database {
|
||||
Err(anyhow!("not a collaborator on this project"))?;
|
||||
}
|
||||
|
||||
let mut collaborators = Vec::new();
|
||||
let mut connections = Vec::new();
|
||||
let mut rows = channel_buffer_collaborator::Entity::find()
|
||||
.filter(
|
||||
@@ -336,19 +371,26 @@ impl Database {
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
connections.push(ConnectionId {
|
||||
id: row.connection_id as u32,
|
||||
owner_id: row.connection_server_id.0 as u32,
|
||||
let connection = row.connection();
|
||||
connections.push(connection);
|
||||
collaborators.push(proto::Collaborator {
|
||||
peer_id: Some(connection.into()),
|
||||
replica_id: row.replica_id.0 as u32,
|
||||
user_id: row.user_id.to_proto(),
|
||||
});
|
||||
}
|
||||
|
||||
drop(rows);
|
||||
|
||||
if connections.is_empty() {
|
||||
if collaborators.is_empty() {
|
||||
self.snapshot_channel_buffer(channel_id, &tx).await?;
|
||||
}
|
||||
|
||||
Ok(connections)
|
||||
Ok(LeftChannelBuffer {
|
||||
channel_id,
|
||||
collaborators,
|
||||
connections,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_channel_buffer_collaborators(
|
||||
@@ -356,33 +398,46 @@ impl Database {
|
||||
channel_id: ChannelId,
|
||||
) -> Result<Vec<UserId>> {
|
||||
self.transaction(|tx| async move {
|
||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||
enum QueryUserIds {
|
||||
UserId,
|
||||
}
|
||||
|
||||
let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
|
||||
.select_only()
|
||||
.column(channel_buffer_collaborator::Column::UserId)
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
|
||||
)
|
||||
.into_values::<_, QueryUserIds>()
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
self.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_channel_buffer_collaborators_internal(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<UserId>> {
|
||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||
enum QueryUserIds {
|
||||
UserId,
|
||||
}
|
||||
|
||||
let users: Vec<UserId> = channel_buffer_collaborator::Entity::find()
|
||||
.select_only()
|
||||
.column(channel_buffer_collaborator::Column::UserId)
|
||||
.filter(
|
||||
Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)),
|
||||
)
|
||||
.into_values::<_, QueryUserIds>()
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(users)
|
||||
}
|
||||
|
||||
pub async fn update_channel_buffer(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user: UserId,
|
||||
operations: &[proto::Operation],
|
||||
) -> Result<Vec<ConnectionId>> {
|
||||
) -> Result<(
|
||||
Vec<ConnectionId>,
|
||||
Vec<UserId>,
|
||||
i32,
|
||||
Vec<proto::VectorClockEntry>,
|
||||
)> {
|
||||
self.transaction(move |tx| async move {
|
||||
self.check_user_is_channel_member(channel_id, user, &*tx)
|
||||
.await?;
|
||||
@@ -401,7 +456,38 @@ impl Database {
|
||||
.iter()
|
||||
.filter_map(|op| operation_to_storage(op, &buffer, serialization_version))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut channel_members;
|
||||
let max_version;
|
||||
|
||||
if !operations.is_empty() {
|
||||
let max_operation = operations
|
||||
.iter()
|
||||
.max_by_key(|op| (op.lamport_timestamp.as_ref(), op.replica_id.as_ref()))
|
||||
.unwrap();
|
||||
|
||||
max_version = vec![proto::VectorClockEntry {
|
||||
replica_id: *max_operation.replica_id.as_ref() as u32,
|
||||
timestamp: *max_operation.lamport_timestamp.as_ref() as u32,
|
||||
}];
|
||||
|
||||
// get current channel participants and save the max operation above
|
||||
self.save_max_operation(
|
||||
user,
|
||||
buffer.id,
|
||||
buffer.epoch,
|
||||
*max_operation.replica_id.as_ref(),
|
||||
*max_operation.lamport_timestamp.as_ref(),
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
||||
let collaborators = self
|
||||
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
||||
.await?;
|
||||
channel_members.retain(|member| !collaborators.contains(member));
|
||||
|
||||
buffer_operation::Entity::insert_many(operations)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
@@ -415,6 +501,9 @@ impl Database {
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
} else {
|
||||
channel_members = Vec::new();
|
||||
max_version = Vec::new();
|
||||
}
|
||||
|
||||
let mut connections = Vec::new();
|
||||
@@ -433,11 +522,53 @@ impl Database {
|
||||
});
|
||||
}
|
||||
|
||||
Ok(connections)
|
||||
Ok((connections, channel_members, buffer.epoch, max_version))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn save_max_operation(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
buffer_id: BufferId,
|
||||
epoch: i32,
|
||||
replica_id: i32,
|
||||
lamport_timestamp: i32,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
use observed_buffer_edits::Column;
|
||||
|
||||
observed_buffer_edits::Entity::insert(observed_buffer_edits::ActiveModel {
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
buffer_id: ActiveValue::Set(buffer_id),
|
||||
epoch: ActiveValue::Set(epoch),
|
||||
replica_id: ActiveValue::Set(replica_id),
|
||||
lamport_timestamp: ActiveValue::Set(lamport_timestamp),
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([Column::UserId, Column::BufferId])
|
||||
.update_columns([Column::Epoch, Column::LamportTimestamp, Column::ReplicaId])
|
||||
.action_cond_where(
|
||||
Condition::any().add(Column::Epoch.lt(epoch)).add(
|
||||
Condition::all().add(Column::Epoch.eq(epoch)).add(
|
||||
Condition::any()
|
||||
.add(Column::LamportTimestamp.lt(lamport_timestamp))
|
||||
.add(
|
||||
Column::LamportTimestamp
|
||||
.eq(lamport_timestamp)
|
||||
.and(Column::ReplicaId.lt(replica_id)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.exec_without_returning(tx)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_buffer_operation_serialization_version(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
@@ -455,7 +586,7 @@ impl Database {
|
||||
.ok_or_else(|| anyhow!("missing buffer snapshot"))?)
|
||||
}
|
||||
|
||||
async fn get_channel_buffer(
|
||||
pub async fn get_channel_buffer(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
tx: &DatabaseTransaction,
|
||||
@@ -474,7 +605,11 @@ impl Database {
|
||||
&self,
|
||||
buffer: &buffer::Model,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<(String, Vec<proto::Operation>)> {
|
||||
) -> Result<(
|
||||
String,
|
||||
Vec<proto::Operation>,
|
||||
Option<buffer_operation::Model>,
|
||||
)> {
|
||||
let id = buffer.id;
|
||||
let (base_text, version) = if buffer.epoch > 0 {
|
||||
let snapshot = buffer_snapshot::Entity::find()
|
||||
@@ -499,16 +634,28 @@ impl Database {
|
||||
.eq(id)
|
||||
.and(buffer_operation::Column::Epoch.eq(buffer.epoch)),
|
||||
)
|
||||
.order_by_asc(buffer_operation::Column::LamportTimestamp)
|
||||
.order_by_asc(buffer_operation::Column::ReplicaId)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut operations = Vec::new();
|
||||
let mut last_row = None;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
last_row = Some(buffer_operation::Model {
|
||||
buffer_id: row.buffer_id,
|
||||
epoch: row.epoch,
|
||||
lamport_timestamp: row.lamport_timestamp,
|
||||
replica_id: row.lamport_timestamp,
|
||||
value: Default::default(),
|
||||
});
|
||||
operations.push(proto::Operation {
|
||||
variant: Some(operation_from_storage(row?, version)?),
|
||||
})
|
||||
variant: Some(operation_from_storage(row, version)?),
|
||||
});
|
||||
}
|
||||
|
||||
Ok((base_text, operations))
|
||||
Ok((base_text, operations, last_row))
|
||||
}
|
||||
|
||||
async fn snapshot_channel_buffer(
|
||||
@@ -517,7 +664,7 @@ impl Database {
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
let buffer = self.get_channel_buffer(channel_id, tx).await?;
|
||||
let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?;
|
||||
let (base_text, operations, _) = self.get_buffer_state(&buffer, tx).await?;
|
||||
if operations.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -550,6 +697,150 @@ impl Database {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn observe_buffer_version(
|
||||
&self,
|
||||
buffer_id: BufferId,
|
||||
user_id: UserId,
|
||||
epoch: i32,
|
||||
version: &[proto::VectorClockEntry],
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
// For now, combine concurrent operations.
|
||||
let Some(component) = version.iter().max_by_key(|version| version.timestamp) else {
|
||||
return Ok(());
|
||||
};
|
||||
self.save_max_operation(
|
||||
user_id,
|
||||
buffer_id,
|
||||
epoch,
|
||||
component.replica_id as i32,
|
||||
component.timestamp as i32,
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn unseen_channel_buffer_changes(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
channel_ids: &[ChannelId],
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::UnseenChannelBufferChange>> {
|
||||
#[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)]
|
||||
enum QueryIds {
|
||||
ChannelId,
|
||||
Id,
|
||||
}
|
||||
|
||||
let mut channel_ids_by_buffer_id = HashMap::default();
|
||||
let mut rows = buffer::Entity::find()
|
||||
.filter(buffer::Column::ChannelId.is_in(channel_ids.iter().copied()))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
channel_ids_by_buffer_id.insert(row.id, row.channel_id);
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let mut observed_edits_by_buffer_id = HashMap::default();
|
||||
let mut rows = observed_buffer_edits::Entity::find()
|
||||
.filter(observed_buffer_edits::Column::UserId.eq(user_id))
|
||||
.filter(
|
||||
observed_buffer_edits::Column::BufferId
|
||||
.is_in(channel_ids_by_buffer_id.keys().copied()),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
observed_edits_by_buffer_id.insert(row.buffer_id, row);
|
||||
}
|
||||
drop(rows);
|
||||
|
||||
let latest_operations = self
|
||||
.get_latest_operations_for_buffers(channel_ids_by_buffer_id.keys().copied(), &*tx)
|
||||
.await?;
|
||||
|
||||
let mut changes = Vec::default();
|
||||
for latest in latest_operations {
|
||||
if let Some(observed) = observed_edits_by_buffer_id.get(&latest.buffer_id) {
|
||||
if (
|
||||
observed.epoch,
|
||||
observed.lamport_timestamp,
|
||||
observed.replica_id,
|
||||
) >= (latest.epoch, latest.lamport_timestamp, latest.replica_id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_ids_by_buffer_id.get(&latest.buffer_id) {
|
||||
changes.push(proto::UnseenChannelBufferChange {
|
||||
channel_id: channel_id.to_proto(),
|
||||
epoch: latest.epoch as u64,
|
||||
version: vec![proto::VectorClockEntry {
|
||||
replica_id: latest.replica_id as u32,
|
||||
timestamp: latest.lamport_timestamp as u32,
|
||||
}],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
pub async fn get_latest_operations_for_buffers(
|
||||
&self,
|
||||
buffer_ids: impl IntoIterator<Item = BufferId>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<buffer_operation::Model>> {
|
||||
let mut values = String::new();
|
||||
for id in buffer_ids {
|
||||
if !values.is_empty() {
|
||||
values.push_str(", ");
|
||||
}
|
||||
write!(&mut values, "({})", id).unwrap();
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
return Ok(Vec::default());
|
||||
}
|
||||
|
||||
let sql = format!(
|
||||
r#"
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
*,
|
||||
row_number() OVER (
|
||||
PARTITION BY buffer_id
|
||||
ORDER BY
|
||||
epoch DESC,
|
||||
lamport_timestamp DESC,
|
||||
replica_id DESC
|
||||
) as row_number
|
||||
FROM buffer_operations
|
||||
WHERE
|
||||
buffer_id in ({values})
|
||||
) AS last_operations
|
||||
WHERE
|
||||
row_number = 1
|
||||
"#,
|
||||
);
|
||||
|
||||
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
|
||||
Ok(buffer_operation::Entity::find()
|
||||
.from_raw_sql(stmt)
|
||||
.all(&*tx)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
|
||||
fn operation_to_storage(
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use super::*;
|
||||
use rpc::proto::ChannelEdge;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::*;
|
||||
|
||||
type ChannelDescendants = HashMap<ChannelId, SmallSet<ChannelId>>;
|
||||
|
||||
impl Database {
|
||||
@@ -20,21 +19,14 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_root_channel(
|
||||
&self,
|
||||
name: &str,
|
||||
live_kit_room: &str,
|
||||
creator_id: UserId,
|
||||
) -> Result<ChannelId> {
|
||||
self.create_channel(name, None, live_kit_room, creator_id)
|
||||
.await
|
||||
pub async fn create_root_channel(&self, name: &str, creator_id: UserId) -> Result<ChannelId> {
|
||||
self.create_channel(name, None, creator_id).await
|
||||
}
|
||||
|
||||
pub async fn create_channel(
|
||||
&self,
|
||||
name: &str,
|
||||
parent: Option<ChannelId>,
|
||||
live_kit_room: &str,
|
||||
creator_id: UserId,
|
||||
) -> Result<ChannelId> {
|
||||
let name = Self::sanitize_channel_name(name)?;
|
||||
@@ -91,14 +83,6 @@ impl Database {
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
room::ActiveModel {
|
||||
channel_id: ActiveValue::Set(Some(channel.id)),
|
||||
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
|
||||
Ok(channel.id)
|
||||
})
|
||||
.await
|
||||
@@ -391,7 +375,8 @@ impl Database {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
self.get_user_channels(channel_memberships, &tx).await
|
||||
self.get_user_channels(user_id, channel_memberships, &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -414,13 +399,15 @@ impl Database {
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
self.get_user_channels(channel_membership, &tx).await
|
||||
self.get_user_channels(user_id, channel_membership, &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_user_channels(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
channel_memberships: Vec<channel_member::Model>,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<ChannelsForUser> {
|
||||
@@ -460,10 +447,21 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
let channel_ids = graph.channels.iter().map(|c| c.id).collect::<Vec<_>>();
|
||||
let channel_buffer_changes = self
|
||||
.unseen_channel_buffer_changes(user_id, &channel_ids, &*tx)
|
||||
.await?;
|
||||
|
||||
let unseen_messages = self
|
||||
.unseen_channel_messages(user_id, &channel_ids, &*tx)
|
||||
.await?;
|
||||
|
||||
Ok(ChannelsForUser {
|
||||
channels: graph,
|
||||
channel_participants,
|
||||
channels_with_admin_privileges,
|
||||
unseen_buffer_changes: channel_buffer_changes,
|
||||
channel_messages: unseen_messages,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -645,7 +643,7 @@ impl Database {
|
||||
) -> Result<Vec<ChannelId>> {
|
||||
let paths = channel_path::Entity::find()
|
||||
.filter(channel_path::Column::ChannelId.eq(channel_id))
|
||||
.order_by(channel_path::Column::IdPath, sea_query::Order::Desc)
|
||||
.order_by(channel_path::Column::IdPath, sea_orm::Order::Desc)
|
||||
.all(tx)
|
||||
.await?;
|
||||
let mut channel_ids = Vec::new();
|
||||
@@ -784,18 +782,36 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn room_id_for_channel(&self, channel_id: ChannelId) -> Result<RoomId> {
|
||||
pub async fn get_or_create_channel_room(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
live_kit_room: &str,
|
||||
enviroment: &str,
|
||||
) -> Result<RoomId> {
|
||||
self.transaction(|tx| async move {
|
||||
let tx = tx;
|
||||
let room = channel::Model {
|
||||
id: channel_id,
|
||||
..Default::default()
|
||||
}
|
||||
.find_related(room::Entity)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("invalid channel"))?;
|
||||
Ok(room.id)
|
||||
|
||||
let room = room::Entity::find()
|
||||
.filter(room::Column::ChannelId.eq(channel_id))
|
||||
.one(&*tx)
|
||||
.await?;
|
||||
|
||||
let room_id = if let Some(room) = room {
|
||||
room.id
|
||||
} else {
|
||||
let result = room::Entity::insert(room::ActiveModel {
|
||||
channel_id: ActiveValue::Set(Some(channel_id)),
|
||||
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
|
||||
enviroment: ActiveValue::Set(Some(enviroment.to_string())),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
result.last_insert_id
|
||||
};
|
||||
|
||||
Ok(room_id)
|
||||
})
|
||||
.await
|
||||
}
|
||||
@@ -828,68 +844,53 @@ impl Database {
|
||||
) -> Result<ChannelGraph> {
|
||||
self.check_user_is_channel_admin(to, user, &*tx).await?;
|
||||
|
||||
let to_ancestors = self.get_channel_ancestors(to, &*tx).await?;
|
||||
let paths = channel_path::Entity::find()
|
||||
.filter(channel_path::Column::IdPath.like(&format!("%/{}/%", channel)))
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let mut new_path_suffixes = HashSet::default();
|
||||
for path in paths {
|
||||
if let Some(start_offset) = path.id_path.find(&format!("/{}/", channel)) {
|
||||
new_path_suffixes.insert((
|
||||
path.channel_id,
|
||||
path.id_path[(start_offset + 1)..].to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let paths_to_new_parent = channel_path::Entity::find()
|
||||
.filter(channel_path::Column::ChannelId.eq(to))
|
||||
.all(tx)
|
||||
.await?;
|
||||
|
||||
let mut new_paths = Vec::new();
|
||||
for path in paths_to_new_parent {
|
||||
if path.id_path.contains(&format!("/{}/", channel)) {
|
||||
Err(anyhow!("cycle"))?;
|
||||
}
|
||||
|
||||
new_paths.extend(new_path_suffixes.iter().map(|(channel_id, path_suffix)| {
|
||||
channel_path::ActiveModel {
|
||||
channel_id: ActiveValue::Set(*channel_id),
|
||||
id_path: ActiveValue::Set(format!("{}{}", &path.id_path, path_suffix)),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
channel_path::Entity::insert_many(new_paths)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
// remove any root edges for the channel we just linked
|
||||
{
|
||||
channel_path::Entity::delete_many()
|
||||
.filter(channel_path::Column::IdPath.like(&format!("/{}/%", channel)))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut channel_descendants = self.get_channel_descendants([channel], &*tx).await?;
|
||||
for ancestor in to_ancestors {
|
||||
if channel_descendants.contains_key(&ancestor) {
|
||||
return Err(anyhow!("Cannot create a channel cycle").into());
|
||||
}
|
||||
}
|
||||
|
||||
// Now insert all of the new paths
|
||||
let sql = r#"
|
||||
INSERT INTO channel_paths
|
||||
(id_path, channel_id)
|
||||
SELECT
|
||||
id_path || $1 || '/', $2
|
||||
FROM
|
||||
channel_paths
|
||||
WHERE
|
||||
channel_id = $3
|
||||
ON CONFLICT (id_path) DO NOTHING;
|
||||
"#;
|
||||
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
sql,
|
||||
[
|
||||
channel.to_proto().into(),
|
||||
channel.to_proto().into(),
|
||||
to.to_proto().into(),
|
||||
],
|
||||
);
|
||||
tx.execute(channel_paths_stmt).await?;
|
||||
for (descdenant_id, descendant_parent_ids) in
|
||||
channel_descendants.iter().filter(|(id, _)| id != &&channel)
|
||||
{
|
||||
for descendant_parent_id in descendant_parent_ids.iter() {
|
||||
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
sql,
|
||||
[
|
||||
descdenant_id.to_proto().into(),
|
||||
descdenant_id.to_proto().into(),
|
||||
descendant_parent_id.to_proto().into(),
|
||||
],
|
||||
);
|
||||
tx.execute(channel_paths_stmt).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// If we're linking a channel, remove any root edges for the channel
|
||||
{
|
||||
let sql = r#"
|
||||
DELETE FROM channel_paths
|
||||
WHERE
|
||||
id_path = '/' || $1 || '/'
|
||||
"#;
|
||||
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
sql,
|
||||
[channel.to_proto().into()],
|
||||
);
|
||||
tx.execute(channel_paths_stmt).await?;
|
||||
}
|
||||
|
||||
if let Some(channel) = channel_descendants.get_mut(&channel) {
|
||||
// Remove the other parents
|
||||
channel.clear();
|
||||
@@ -936,35 +937,43 @@ impl Database {
|
||||
self.check_user_is_channel_admin(from, user, &*tx).await?;
|
||||
|
||||
let sql = r#"
|
||||
DELETE FROM channel_paths
|
||||
WHERE
|
||||
id_path LIKE '%' || $1 || '/' || $2 || '%'
|
||||
"#;
|
||||
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
sql,
|
||||
[from.to_proto().into(), channel.to_proto().into()],
|
||||
);
|
||||
tx.execute(channel_paths_stmt).await?;
|
||||
DELETE FROM channel_paths
|
||||
WHERE
|
||||
id_path LIKE '%/' || $1 || '/' || $2 || '/%'
|
||||
RETURNING id_path, channel_id
|
||||
"#;
|
||||
|
||||
let paths = channel_path::Entity::find()
|
||||
.from_raw_sql(Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
sql,
|
||||
[from.to_proto().into(), channel.to_proto().into()],
|
||||
))
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let is_stranded = channel_path::Entity::find()
|
||||
.filter(channel_path::Column::ChannelId.eq(channel))
|
||||
.count(&*tx)
|
||||
.await?
|
||||
== 0;
|
||||
|
||||
// Make sure that there is always at least one path to the channel
|
||||
let sql = r#"
|
||||
INSERT INTO channel_paths
|
||||
(id_path, channel_id)
|
||||
SELECT
|
||||
'/' || $1 || '/', $2
|
||||
WHERE NOT EXISTS
|
||||
(SELECT *
|
||||
FROM channel_paths
|
||||
WHERE channel_id = $2)
|
||||
"#;
|
||||
|
||||
let channel_paths_stmt = Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
sql,
|
||||
[channel.to_proto().into(), channel.to_proto().into()],
|
||||
);
|
||||
tx.execute(channel_paths_stmt).await?;
|
||||
if is_stranded {
|
||||
let root_paths: Vec<_> = paths
|
||||
.iter()
|
||||
.map(|path| {
|
||||
let start_offset = path.id_path.find(&format!("/{}/", channel)).unwrap();
|
||||
channel_path::ActiveModel {
|
||||
channel_id: ActiveValue::Set(path.channel_id),
|
||||
id_path: ActiveValue::Set(path.id_path[start_offset..].to_string()),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
channel_path::Entity::insert_many(root_paths)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -978,6 +987,13 @@ impl Database {
|
||||
from: ChannelId,
|
||||
to: ChannelId,
|
||||
) -> Result<ChannelGraph> {
|
||||
if from == to {
|
||||
return Ok(ChannelGraph {
|
||||
channels: vec![],
|
||||
edges: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
self.transaction(|tx| async move {
|
||||
self.check_user_is_channel_admin(channel, user, &*tx)
|
||||
.await?;
|
||||
|
||||
@@ -18,12 +18,12 @@ impl Database {
|
||||
let user_b_participant = Alias::new("user_b_participant");
|
||||
let mut db_contacts = contact::Entity::find()
|
||||
.column_as(
|
||||
Expr::tbl(user_a_participant.clone(), room_participant::Column::Id)
|
||||
Expr::col((user_a_participant.clone(), room_participant::Column::Id))
|
||||
.is_not_null(),
|
||||
"user_a_busy",
|
||||
)
|
||||
.column_as(
|
||||
Expr::tbl(user_b_participant.clone(), room_participant::Column::Id)
|
||||
Expr::col((user_b_participant.clone(), room_participant::Column::Id))
|
||||
.is_not_null(),
|
||||
"user_b_busy",
|
||||
)
|
||||
|
||||
@@ -89,6 +89,7 @@ impl Database {
|
||||
|
||||
let mut rows = channel_message::Entity::find()
|
||||
.filter(condition)
|
||||
.order_by_desc(channel_message::Column::Id)
|
||||
.limit(count as u64)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
@@ -108,7 +109,8 @@ impl Database {
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
drop(rows);
|
||||
messages.reverse();
|
||||
Ok(messages)
|
||||
})
|
||||
.await
|
||||
@@ -121,7 +123,7 @@ impl Database {
|
||||
body: &str,
|
||||
timestamp: OffsetDateTime,
|
||||
nonce: u128,
|
||||
) -> Result<(MessageId, Vec<ConnectionId>)> {
|
||||
) -> Result<(MessageId, Vec<ConnectionId>, Vec<UserId>)> {
|
||||
self.transaction(|tx| async move {
|
||||
let mut rows = channel_chat_participant::Entity::find()
|
||||
.filter(channel_chat_participant::Column::ChannelId.eq(channel_id))
|
||||
@@ -130,11 +132,13 @@ impl Database {
|
||||
|
||||
let mut is_participant = false;
|
||||
let mut participant_connection_ids = Vec::new();
|
||||
let mut participant_user_ids = Vec::new();
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
if row.user_id == user_id {
|
||||
is_participant = true;
|
||||
}
|
||||
participant_user_ids.push(row.user_id);
|
||||
participant_connection_ids.push(row.connection());
|
||||
}
|
||||
drop(rows);
|
||||
@@ -167,11 +171,141 @@ impl Database {
|
||||
ConnectionId,
|
||||
}
|
||||
|
||||
Ok((message.last_insert_id, participant_connection_ids))
|
||||
// Observe this message for the sender
|
||||
self.observe_channel_message_internal(
|
||||
channel_id,
|
||||
user_id,
|
||||
message.last_insert_id,
|
||||
&*tx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
||||
channel_members.retain(|member| !participant_user_ids.contains(member));
|
||||
|
||||
Ok((
|
||||
message.last_insert_id,
|
||||
participant_connection_ids,
|
||||
channel_members,
|
||||
))
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn observe_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
message_id: MessageId,
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn observe_channel_message_internal(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
message_id: MessageId,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<()> {
|
||||
observed_channel_messages::Entity::insert(observed_channel_messages::ActiveModel {
|
||||
user_id: ActiveValue::Set(user_id),
|
||||
channel_id: ActiveValue::Set(channel_id),
|
||||
channel_message_id: ActiveValue::Set(message_id),
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
observed_channel_messages::Column::ChannelId,
|
||||
observed_channel_messages::Column::UserId,
|
||||
])
|
||||
.update_column(observed_channel_messages::Column::ChannelMessageId)
|
||||
.action_cond_where(observed_channel_messages::Column::ChannelMessageId.lt(message_id))
|
||||
.to_owned(),
|
||||
)
|
||||
// TODO: Try to upgrade SeaORM so we don't have to do this hack around their bug
|
||||
.exec_without_returning(&*tx)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unseen_channel_messages(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
channel_ids: &[ChannelId],
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<Vec<proto::UnseenChannelMessage>> {
|
||||
let mut observed_messages_by_channel_id = HashMap::default();
|
||||
let mut rows = observed_channel_messages::Entity::find()
|
||||
.filter(observed_channel_messages::Column::UserId.eq(user_id))
|
||||
.filter(observed_channel_messages::Column::ChannelId.is_in(channel_ids.iter().copied()))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(row) = rows.next().await {
|
||||
let row = row?;
|
||||
observed_messages_by_channel_id.insert(row.channel_id, row);
|
||||
}
|
||||
drop(rows);
|
||||
let mut values = String::new();
|
||||
for id in channel_ids {
|
||||
if !values.is_empty() {
|
||||
values.push_str(", ");
|
||||
}
|
||||
write!(&mut values, "({})", id).unwrap();
|
||||
}
|
||||
|
||||
if values.is_empty() {
|
||||
return Ok(Default::default());
|
||||
}
|
||||
|
||||
let sql = format!(
|
||||
r#"
|
||||
SELECT
|
||||
*
|
||||
FROM (
|
||||
SELECT
|
||||
*,
|
||||
row_number() OVER (
|
||||
PARTITION BY channel_id
|
||||
ORDER BY id DESC
|
||||
) as row_number
|
||||
FROM channel_messages
|
||||
WHERE
|
||||
channel_id in ({values})
|
||||
) AS messages
|
||||
WHERE
|
||||
row_number = 1
|
||||
"#,
|
||||
);
|
||||
|
||||
let stmt = Statement::from_string(self.pool.get_database_backend(), sql);
|
||||
let last_messages = channel_message::Model::find_by_statement(stmt)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut changes = Vec::new();
|
||||
for last_message in last_messages {
|
||||
if let Some(observed_message) =
|
||||
observed_messages_by_channel_id.get(&last_message.channel_id)
|
||||
{
|
||||
if observed_message.channel_message_id == last_message.id {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
changes.push(proto::UnseenChannelMessage {
|
||||
channel_id: last_message.channel_id.to_proto(),
|
||||
message_id: last_message.id.to_proto(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
pub async fn remove_channel_message(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
|
||||
@@ -738,7 +738,7 @@ impl Database {
|
||||
Condition::any()
|
||||
.add(
|
||||
Condition::all()
|
||||
.add(follower::Column::ProjectId.eq(project_id))
|
||||
.add(follower::Column::ProjectId.eq(Some(project_id)))
|
||||
.add(
|
||||
follower::Column::LeaderConnectionServerId
|
||||
.eq(connection.owner_id),
|
||||
@@ -747,7 +747,7 @@ impl Database {
|
||||
)
|
||||
.add(
|
||||
Condition::all()
|
||||
.add(follower::Column::ProjectId.eq(project_id))
|
||||
.add(follower::Column::ProjectId.eq(Some(project_id)))
|
||||
.add(
|
||||
follower::Column::FollowerConnectionServerId
|
||||
.eq(connection.owner_id),
|
||||
@@ -862,13 +862,46 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn check_room_participants(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
leader_id: ConnectionId,
|
||||
follower_id: ConnectionId,
|
||||
) -> Result<()> {
|
||||
self.transaction(|tx| async move {
|
||||
use room_participant::Column;
|
||||
|
||||
let count = room_participant::Entity::find()
|
||||
.filter(
|
||||
Condition::all().add(Column::RoomId.eq(room_id)).add(
|
||||
Condition::any()
|
||||
.add(Column::AnsweringConnectionId.eq(leader_id.id as i32).and(
|
||||
Column::AnsweringConnectionServerId.eq(leader_id.owner_id as i32),
|
||||
))
|
||||
.add(Column::AnsweringConnectionId.eq(follower_id.id as i32).and(
|
||||
Column::AnsweringConnectionServerId.eq(follower_id.owner_id as i32),
|
||||
)),
|
||||
),
|
||||
)
|
||||
.count(&*tx)
|
||||
.await?;
|
||||
|
||||
if count < 2 {
|
||||
Err(anyhow!("not room participants"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn follow(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
project_id: ProjectId,
|
||||
leader_connection: ConnectionId,
|
||||
follower_connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
follower::ActiveModel {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
@@ -894,15 +927,16 @@ impl Database {
|
||||
|
||||
pub async fn unfollow(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
project_id: ProjectId,
|
||||
leader_connection: ConnectionId,
|
||||
follower_connection: ConnectionId,
|
||||
) -> Result<RoomGuard<proto::Room>> {
|
||||
let room_id = self.room_id_for_project(project_id).await?;
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
follower::Entity::delete_many()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(follower::Column::RoomId.eq(room_id))
|
||||
.add(follower::Column::ProjectId.eq(project_id))
|
||||
.add(
|
||||
follower::Column::LeaderConnectionServerId
|
||||
|
||||
@@ -107,10 +107,12 @@ impl Database {
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
live_kit_room: &str,
|
||||
release_channel: &str,
|
||||
) -> Result<proto::Room> {
|
||||
self.transaction(|tx| async move {
|
||||
let room = room::ActiveModel {
|
||||
live_kit_room: ActiveValue::set(live_kit_room.into()),
|
||||
enviroment: ActiveValue::set(Some(release_channel.to_string())),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
@@ -128,6 +130,7 @@ impl Database {
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
participant_index: ActiveValue::set(Some(0)),
|
||||
..Default::default()
|
||||
}
|
||||
.insert(&*tx)
|
||||
@@ -152,6 +155,7 @@ impl Database {
|
||||
room_id: ActiveValue::set(room_id),
|
||||
user_id: ActiveValue::set(called_user_id),
|
||||
answering_connection_lost: ActiveValue::set(false),
|
||||
participant_index: ActiveValue::NotSet,
|
||||
calling_user_id: ActiveValue::set(calling_user_id),
|
||||
calling_connection_id: ActiveValue::set(calling_connection.id as i32),
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
@@ -268,20 +272,52 @@ impl Database {
|
||||
room_id: RoomId,
|
||||
user_id: UserId,
|
||||
connection: ConnectionId,
|
||||
enviroment: &str,
|
||||
) -> Result<RoomGuard<JoinRoom>> {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryChannelId {
|
||||
enum QueryChannelIdAndEnviroment {
|
||||
ChannelId,
|
||||
Enviroment,
|
||||
}
|
||||
let channel_id: Option<ChannelId> = room::Entity::find()
|
||||
|
||||
let (channel_id, release_channel): (Option<ChannelId>, Option<String>) =
|
||||
room::Entity::find()
|
||||
.select_only()
|
||||
.column(room::Column::ChannelId)
|
||||
.column(room::Column::Enviroment)
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.into_values::<_, QueryChannelIdAndEnviroment>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
|
||||
if let Some(release_channel) = release_channel {
|
||||
if &release_channel != enviroment {
|
||||
Err(anyhow!("must join using the {} release", release_channel))?;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryParticipantIndices {
|
||||
ParticipantIndex,
|
||||
}
|
||||
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||
.filter(
|
||||
room_participant::Column::RoomId
|
||||
.eq(room_id)
|
||||
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||
)
|
||||
.select_only()
|
||||
.column(room::Column::ChannelId)
|
||||
.filter(room::Column::Id.eq(room_id))
|
||||
.into_values::<_, QueryChannelId>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such room"))?;
|
||||
.column(room_participant::Column::ParticipantIndex)
|
||||
.into_values::<_, QueryParticipantIndices>()
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut participant_index = 0;
|
||||
while existing_participant_indices.contains(&participant_index) {
|
||||
participant_index += 1;
|
||||
}
|
||||
|
||||
if let Some(channel_id) = channel_id {
|
||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
||||
@@ -300,6 +336,7 @@ impl Database {
|
||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
))),
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
..Default::default()
|
||||
}])
|
||||
.on_conflict(
|
||||
@@ -308,6 +345,7 @@ impl Database {
|
||||
room_participant::Column::AnsweringConnectionId,
|
||||
room_participant::Column::AnsweringConnectionServerId,
|
||||
room_participant::Column::AnsweringConnectionLost,
|
||||
room_participant::Column::ParticipantIndex,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
@@ -322,6 +360,7 @@ impl Database {
|
||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||
)
|
||||
.set(room_participant::ActiveModel {
|
||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||
connection.owner_id as i32,
|
||||
@@ -793,10 +832,7 @@ impl Database {
|
||||
|
||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||
let deleted = if room.participants.is_empty() {
|
||||
let result = room::Entity::delete_by_id(room_id)
|
||||
.filter(room::Column::ChannelId.is_null())
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
let result = room::Entity::delete_by_id(room_id).exec(&*tx).await?;
|
||||
result.rows_affected > 0
|
||||
} else {
|
||||
false
|
||||
@@ -960,6 +996,39 @@ impl Database {
|
||||
Ok(room)
|
||||
}
|
||||
|
||||
pub async fn room_connection_ids(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<RoomGuard<HashSet<ConnectionId>>> {
|
||||
self.room_transaction(room_id, |tx| async move {
|
||||
let mut participants = room_participant::Entity::find()
|
||||
.filter(room_participant::Column::RoomId.eq(room_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut is_participant = false;
|
||||
let mut connection_ids = HashSet::default();
|
||||
while let Some(participant) = participants.next().await {
|
||||
let participant = participant?;
|
||||
if let Some(answering_connection) = participant.answering_connection() {
|
||||
if answering_connection == connection_id {
|
||||
is_participant = true;
|
||||
} else {
|
||||
connection_ids.insert(answering_connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !is_participant {
|
||||
Err(anyhow!("not a room participant"))?;
|
||||
}
|
||||
|
||||
Ok(connection_ids)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_channel_room(
|
||||
&self,
|
||||
room_id: RoomId,
|
||||
@@ -978,10 +1047,15 @@ impl Database {
|
||||
let mut pending_participants = Vec::new();
|
||||
while let Some(db_participant) = db_participants.next().await {
|
||||
let db_participant = db_participant?;
|
||||
if let Some((answering_connection_id, answering_connection_server_id)) = db_participant
|
||||
.answering_connection_id
|
||||
.zip(db_participant.answering_connection_server_id)
|
||||
{
|
||||
if let (
|
||||
Some(answering_connection_id),
|
||||
Some(answering_connection_server_id),
|
||||
Some(participant_index),
|
||||
) = (
|
||||
db_participant.answering_connection_id,
|
||||
db_participant.answering_connection_server_id,
|
||||
db_participant.participant_index,
|
||||
) {
|
||||
let location = match (
|
||||
db_participant.location_kind,
|
||||
db_participant.location_project_id,
|
||||
@@ -1012,6 +1086,7 @@ impl Database {
|
||||
peer_id: Some(answering_connection.into()),
|
||||
projects: Default::default(),
|
||||
location: Some(proto::ParticipantLocation { variant: location }),
|
||||
participant_index: participant_index as u32,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -184,7 +184,7 @@ impl Database {
|
||||
Ok(user::Entity::find()
|
||||
.from_raw_sql(Statement::from_sql_and_values(
|
||||
self.pool.get_database_backend(),
|
||||
query.into(),
|
||||
query,
|
||||
vec![like_string.into(), name_query.into(), limit.into()],
|
||||
))
|
||||
.all(&*tx)
|
||||
|
||||
@@ -12,6 +12,8 @@ pub mod contact;
|
||||
pub mod feature_flag;
|
||||
pub mod follower;
|
||||
pub mod language_server;
|
||||
pub mod observed_buffer_edits;
|
||||
pub mod observed_channel_messages;
|
||||
pub mod project;
|
||||
pub mod project_collaborator;
|
||||
pub mod room;
|
||||
|
||||
43
crates/collab/src/db/tables/observed_buffer_edits.rs
Normal file
43
crates/collab/src/db/tables/observed_buffer_edits.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use crate::db::{BufferId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "observed_buffer_edits")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub user_id: UserId,
|
||||
pub buffer_id: BufferId,
|
||||
pub epoch: i32,
|
||||
pub lamport_timestamp: i32,
|
||||
pub replica_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::buffer::Entity",
|
||||
from = "Column::BufferId",
|
||||
to = "super::buffer::Column::Id"
|
||||
)]
|
||||
Buffer,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::buffer::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Buffer.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
41
crates/collab/src/db/tables/observed_channel_messages.rs
Normal file
41
crates/collab/src/db/tables/observed_channel_messages.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use crate::db::{ChannelId, MessageId, UserId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "observed_channel_messages")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub user_id: UserId,
|
||||
pub channel_id: ChannelId,
|
||||
pub channel_message_id: MessageId,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::channel::Entity",
|
||||
from = "Column::ChannelId",
|
||||
to = "super::channel::Column::Id"
|
||||
)]
|
||||
Channel,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::channel::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Channel.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -8,6 +8,7 @@ pub struct Model {
|
||||
pub id: RoomId,
|
||||
pub live_kit_room: String,
|
||||
pub channel_id: Option<ChannelId>,
|
||||
pub enviroment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::db::{ProjectId, RoomId, RoomParticipantId, ServerId, UserId};
|
||||
use rpc::ConnectionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
@@ -17,6 +18,16 @@ pub struct Model {
|
||||
pub calling_user_id: UserId,
|
||||
pub calling_connection_id: i32,
|
||||
pub calling_connection_server_id: Option<ServerId>,
|
||||
pub participant_index: Option<i32>,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn answering_connection(&self) -> Option<ConnectionId> {
|
||||
Some(ConnectionId {
|
||||
owner_id: self.answering_connection_server_id?.0 as u32,
|
||||
id: self.answering_connection_id? as u32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -12,6 +12,8 @@ use sea_orm::ConnectionTrait;
|
||||
use sqlx::migrate::MigrateDatabase;
|
||||
use std::sync::Arc;
|
||||
|
||||
const TEST_RELEASE_CHANNEL: &'static str = "test";
|
||||
|
||||
pub struct TestDb {
|
||||
pub db: Option<Arc<Database>>,
|
||||
pub connection: Option<sqlx::AnyConnection>,
|
||||
@@ -39,7 +41,7 @@ impl TestDb {
|
||||
db.pool
|
||||
.execute(sea_orm::Statement::from_string(
|
||||
db.pool.get_database_backend(),
|
||||
sql.into(),
|
||||
sql,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -134,7 +136,7 @@ impl Drop for TestDb {
|
||||
db.pool
|
||||
.execute(sea_orm::Statement::from_string(
|
||||
db.pool.get_database_backend(),
|
||||
query.into(),
|
||||
query,
|
||||
))
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
use crate::test_both_dbs;
|
||||
use language::proto;
|
||||
use language::proto::{self, serialize_version};
|
||||
use text::Buffer;
|
||||
|
||||
test_both_dbs!(
|
||||
@@ -54,7 +54,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
|
||||
let owner_id = db.create_server("production").await.unwrap().0 as u32;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
||||
.await
|
||||
@@ -134,14 +134,14 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap();
|
||||
assert_eq!(zed_collaborats, &[a_id, b_id]);
|
||||
|
||||
let collaborators = db
|
||||
let left_buffer = db
|
||||
.leave_channel_buffer(zed_id, connection_id_b)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(collaborators, &[connection_id_a],);
|
||||
assert_eq!(left_buffer.connections, &[connection_id_a],);
|
||||
|
||||
let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap();
|
||||
let cargo_id = db.create_root_channel("cargo", a_id).await.unwrap();
|
||||
let _ = db
|
||||
.join_channel_buffer(cargo_id, a_id, connection_id_a)
|
||||
.await
|
||||
@@ -163,3 +163,349 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||
assert_eq!(buffer_response_b.base_text, "hello, cruel world");
|
||||
assert_eq!(buffer_response_b.operations, &[]);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_buffers_last_operations,
|
||||
test_channel_buffers_last_operations_postgres,
|
||||
test_channel_buffers_last_operations_sqlite
|
||||
);
|
||||
|
||||
async fn test_channel_buffers_last_operations(db: &Database) {
|
||||
let user_id = db
|
||||
.create_user(
|
||||
"user_a@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user_a".into(),
|
||||
github_user_id: 101,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let observer_id = db
|
||||
.create_user(
|
||||
"user_b@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user_b".into(),
|
||||
github_user_id: 102,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let owner_id = db.create_server("production").await.unwrap().0 as u32;
|
||||
let connection_id = ConnectionId {
|
||||
owner_id,
|
||||
id: user_id.0 as u32,
|
||||
};
|
||||
|
||||
let mut buffers = Vec::new();
|
||||
let mut text_buffers = Vec::new();
|
||||
for i in 0..3 {
|
||||
let channel = db
|
||||
.create_root_channel(&format!("channel-{i}"), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel, observer_id, user_id, false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.respond_to_channel_invite(channel, observer_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.join_channel_buffer(channel, user_id, connection_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
buffers.push(
|
||||
db.transaction(|tx| async move { db.get_channel_buffer(channel, &*tx).await })
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
text_buffers.push(Buffer::new(0, 0, "".to_string()));
|
||||
}
|
||||
|
||||
let operations = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.get_latest_operations_for_buffers([buffers[0].id, buffers[2].id], &*tx)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(operations.is_empty());
|
||||
|
||||
update_buffer(
|
||||
buffers[0].channel_id,
|
||||
user_id,
|
||||
db,
|
||||
vec![
|
||||
text_buffers[0].edit([(0..0, "a")]),
|
||||
text_buffers[0].edit([(0..0, "b")]),
|
||||
text_buffers[0].edit([(0..0, "c")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
update_buffer(
|
||||
buffers[1].channel_id,
|
||||
user_id,
|
||||
db,
|
||||
vec![
|
||||
text_buffers[1].edit([(0..0, "d")]),
|
||||
text_buffers[1].edit([(1..1, "e")]),
|
||||
text_buffers[1].edit([(2..2, "f")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
// cause buffer 1's epoch to increment.
|
||||
db.leave_channel_buffer(buffers[1].channel_id, connection_id)
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_channel_buffer(buffers[1].channel_id, user_id, connection_id)
|
||||
.await
|
||||
.unwrap();
|
||||
text_buffers[1] = Buffer::new(1, 0, "def".to_string());
|
||||
update_buffer(
|
||||
buffers[1].channel_id,
|
||||
user_id,
|
||||
db,
|
||||
vec![
|
||||
text_buffers[1].edit([(0..0, "g")]),
|
||||
text_buffers[1].edit([(0..0, "h")]),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
update_buffer(
|
||||
buffers[2].channel_id,
|
||||
user_id,
|
||||
db,
|
||||
vec![text_buffers[2].edit([(0..0, "i")])],
|
||||
)
|
||||
.await;
|
||||
|
||||
let operations = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.get_latest_operations_for_buffers([buffers[1].id, buffers[2].id], &*tx)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_operations(
|
||||
&operations,
|
||||
&[
|
||||
(buffers[1].id, 1, &text_buffers[1]),
|
||||
(buffers[2].id, 0, &text_buffers[2]),
|
||||
],
|
||||
);
|
||||
|
||||
let operations = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.get_latest_operations_for_buffers([buffers[0].id, buffers[1].id], &*tx)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_operations(
|
||||
&operations,
|
||||
&[
|
||||
(buffers[0].id, 0, &text_buffers[0]),
|
||||
(buffers[1].id, 1, &text_buffers[1]),
|
||||
],
|
||||
);
|
||||
|
||||
let buffer_changes = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
buffers[2].channel_id,
|
||||
],
|
||||
&*tx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[1].channel_id.to_proto(),
|
||||
epoch: 1,
|
||||
version: serialize_version(&text_buffers[1].version())
|
||||
.into_iter()
|
||||
.filter(|vector| vector.replica_id
|
||||
== buffer_changes[1].version.first().unwrap().replica_id)
|
||||
.collect::<Vec<_>>(),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
db.observe_buffer_version(
|
||||
buffers[1].id,
|
||||
observer_id,
|
||||
1,
|
||||
serialize_version(&text_buffers[1].version()).as_slice(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_changes = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
buffers[2].channel_id,
|
||||
],
|
||||
&*tx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe an earlier version of the buffer.
|
||||
db.observe_buffer_version(
|
||||
buffers[1].id,
|
||||
observer_id,
|
||||
1,
|
||||
&[rpc::proto::VectorClockEntry {
|
||||
replica_id: 0,
|
||||
timestamp: 0,
|
||||
}],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_changes = db
|
||||
.transaction(|tx| {
|
||||
let buffers = &buffers;
|
||||
async move {
|
||||
db.unseen_channel_buffer_changes(
|
||||
observer_id,
|
||||
&[
|
||||
buffers[0].channel_id,
|
||||
buffers[1].channel_id,
|
||||
buffers[2].channel_id,
|
||||
],
|
||||
&*tx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
buffer_changes,
|
||||
[
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[0].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[0].version()),
|
||||
},
|
||||
rpc::proto::UnseenChannelBufferChange {
|
||||
channel_id: buffers[2].channel_id.to_proto(),
|
||||
epoch: 0,
|
||||
version: serialize_version(&text_buffers[2].version()),
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
async fn update_buffer(
|
||||
channel_id: ChannelId,
|
||||
user_id: UserId,
|
||||
db: &Database,
|
||||
operations: Vec<text::Operation>,
|
||||
) {
|
||||
let operations = operations
|
||||
.into_iter()
|
||||
.map(|op| proto::serialize_operation(&language::Operation::Buffer(op)))
|
||||
.collect::<Vec<_>>();
|
||||
db.update_channel_buffer(channel_id, user_id, &operations)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn assert_operations(
|
||||
operations: &[buffer_operation::Model],
|
||||
expected: &[(BufferId, i32, &text::Buffer)],
|
||||
) {
|
||||
let actual = operations
|
||||
.iter()
|
||||
.map(|op| buffer_operation::Model {
|
||||
buffer_id: op.buffer_id,
|
||||
epoch: op.epoch,
|
||||
lamport_timestamp: op.lamport_timestamp,
|
||||
replica_id: op.replica_id,
|
||||
value: vec![],
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let expected = expected
|
||||
.iter()
|
||||
.map(|(buffer_id, epoch, buffer)| buffer_operation::Model {
|
||||
buffer_id: *buffer_id,
|
||||
epoch: *epoch,
|
||||
lamport_timestamp: buffer.lamport_clock.value as i32 - 1,
|
||||
replica_id: buffer.replica_id() as i32,
|
||||
value: vec![],
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected, "unexpected operations")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ use rpc::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db::{queries::channels::ChannelGraph, tests::graph, ChannelId, Database, NewUserParams},
|
||||
db::{
|
||||
queries::channels::ChannelGraph,
|
||||
tests::{graph, TEST_RELEASE_CHANNEL},
|
||||
ChannelId, Database, NewUserParams,
|
||||
},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
@@ -41,7 +45,7 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
// Make sure that people cannot read channels they haven't been invited to
|
||||
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
|
||||
@@ -54,16 +58,13 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let crdb_id = db
|
||||
.create_channel("crdb", Some(zed_id), "2", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(zed_id), "3", a_id)
|
||||
.create_channel("livestreaming", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let replace_id = db
|
||||
.create_channel("replace", Some(zed_id), "4", a_id)
|
||||
.create_channel("replace", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -71,14 +72,14 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
members.sort();
|
||||
assert_eq!(members, &[a_id, b_id]);
|
||||
|
||||
let rust_id = db.create_root_channel("rust", "5", a_id).await.unwrap();
|
||||
let rust_id = db.create_root_channel("rust", a_id).await.unwrap();
|
||||
let cargo_id = db
|
||||
.create_channel("cargo", Some(rust_id), "6", a_id)
|
||||
.create_channel("cargo", Some(rust_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cargo_ra_id = db
|
||||
.create_channel("cargo-ra", Some(cargo_id), "7", a_id)
|
||||
.create_channel("cargo-ra", Some(cargo_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -198,15 +199,20 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1 = db
|
||||
.create_root_channel("channel_1", "1", user_1)
|
||||
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
let room_1 = db
|
||||
.get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
|
||||
.await
|
||||
.unwrap();
|
||||
let room_1 = db.room_id_for_channel(channel_1).await.unwrap();
|
||||
|
||||
// can join a room with membership to its channel
|
||||
let joined_room = db
|
||||
.join_room(room_1, user_1, ConnectionId { owner_id, id: 1 })
|
||||
.join_room(
|
||||
room_1,
|
||||
user_1,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(joined_room.room.participants.len(), 1);
|
||||
@@ -214,7 +220,12 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||
drop(joined_room);
|
||||
// cannot join a room without membership to its channel
|
||||
assert!(db
|
||||
.join_room(room_1, user_2, ConnectionId { owner_id, id: 1 })
|
||||
.join_room(
|
||||
room_1,
|
||||
user_2,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
TEST_RELEASE_CHANNEL
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
}
|
||||
@@ -269,15 +280,9 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1_1 = db
|
||||
.create_root_channel("channel_1", "1", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||
|
||||
let channel_1_2 = db
|
||||
.create_root_channel("channel_2", "2", user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1_1, user_2, user_1, false)
|
||||
.await
|
||||
@@ -339,7 +344,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let channel_1_3 = db
|
||||
.create_channel("channel_3", Some(channel_1_1), "1", user_1)
|
||||
.create_channel("channel_3", Some(channel_1_1), user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -401,7 +406,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", user_1).await.unwrap();
|
||||
|
||||
db.rename_channel(zed_id, user_1, "#zed-archive")
|
||||
.await
|
||||
@@ -446,25 +451,22 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap();
|
||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||
|
||||
let crdb_id = db
|
||||
.create_channel("crdb", Some(zed_id), "2", a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let crdb_id = db.create_channel("crdb", Some(zed_id), a_id).await.unwrap();
|
||||
|
||||
let gpui2_id = db
|
||||
.create_channel("gpui2", Some(zed_id), "3", a_id)
|
||||
.create_channel("gpui2", Some(zed_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(crdb_id), "4", a_id)
|
||||
.create_channel("livestreaming", Some(crdb_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_dag_id = db
|
||||
.create_channel("livestreaming_dag", Some(livestreaming_id), "5", a_id)
|
||||
.create_channel("livestreaming_dag", Some(livestreaming_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -517,12 +519,7 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||
// ========================================================================
|
||||
// Create a new channel below a channel with multiple parents
|
||||
let livestreaming_dag_sub_id = db
|
||||
.create_channel(
|
||||
"livestreaming_dag_sub",
|
||||
Some(livestreaming_dag_id),
|
||||
"6",
|
||||
a_id,
|
||||
)
|
||||
.create_channel("livestreaming_dag_sub", Some(livestreaming_dag_id), a_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -791,6 +788,64 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||
assert!(result.channels.is_empty())
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_db_channel_moving_bugs,
|
||||
test_db_channel_moving_bugs_postgres,
|
||||
test_db_channel_moving_bugs_sqlite
|
||||
);
|
||||
|
||||
async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
let user_id = db
|
||||
.create_user(
|
||||
"user1@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user1".into(),
|
||||
github_user_id: 5,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let zed_id = db.create_root_channel("zed", user_id).await.unwrap();
|
||||
|
||||
let projects_id = db
|
||||
.create_channel("projects", Some(zed_id), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let livestreaming_id = db
|
||||
.create_channel("livestreaming", Some(projects_id), user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Dag is: zed - projects - livestreaming
|
||||
|
||||
// Move to same parent should be a no-op
|
||||
assert!(db
|
||||
.move_channel(user_id, projects_id, zed_id, zed_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
|
||||
// Stranding a channel should retain it's sub channels
|
||||
db.unlink_channel(user_id, projects_id, zed_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_dag(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, None),
|
||||
(projects_id, None),
|
||||
(livestreaming_id, Some(projects_id)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
|
||||
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
|
||||
|
||||
@@ -479,7 +479,7 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
.unwrap();
|
||||
|
||||
let room_id = RoomId::from_proto(
|
||||
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "")
|
||||
db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev")
|
||||
.await
|
||||
.unwrap()
|
||||
.id,
|
||||
@@ -493,9 +493,14 @@ async fn test_project_count(db: &Arc<Database>) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 })
|
||||
.await
|
||||
.unwrap();
|
||||
db.join_room(
|
||||
room_id,
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"dev",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0);
|
||||
|
||||
db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[])
|
||||
@@ -575,6 +580,85 @@ async fn test_fuzzy_search_users() {
|
||||
}
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_non_matching_release_channels,
|
||||
test_non_matching_release_channels_postgres,
|
||||
test_non_matching_release_channels_sqlite
|
||||
);
|
||||
|
||||
async fn test_non_matching_release_channels(db: &Arc<Database>) {
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
|
||||
let user1 = db
|
||||
.create_user(
|
||||
&format!("admin@example.com"),
|
||||
true,
|
||||
NewUserParams {
|
||||
github_login: "admin".into(),
|
||||
github_user_id: 0,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let user2 = db
|
||||
.create_user(
|
||||
&format!("user@example.com"),
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room = db
|
||||
.create_room(
|
||||
user1.user_id,
|
||||
ConnectionId { owner_id, id: 0 },
|
||||
"",
|
||||
"stable",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.call(
|
||||
RoomId::from_proto(room.id),
|
||||
user1.user_id,
|
||||
ConnectionId { owner_id, id: 0 },
|
||||
user2.user_id,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// User attempts to join from preview
|
||||
let result = db
|
||||
.join_room(
|
||||
RoomId::from_proto(room.id),
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"preview",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_err());
|
||||
|
||||
// User switches to stable
|
||||
let result = db
|
||||
.join_room(
|
||||
RoomId::from_proto(room.id),
|
||||
user2.user_id,
|
||||
ConnectionId { owner_id, id: 1 },
|
||||
"stable",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(result.is_ok())
|
||||
}
|
||||
|
||||
fn build_background_executor() -> Arc<Background> {
|
||||
Deterministic::new(0).build_background()
|
||||
}
|
||||
|
||||
@@ -1,10 +1,72 @@
|
||||
use crate::{
|
||||
db::{Database, NewUserParams},
|
||||
db::{Database, MessageId, NewUserParams},
|
||||
test_both_dbs,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_message_retrieval,
|
||||
test_channel_message_retrieval_postgres,
|
||||
test_channel_message_retrieval_sqlite
|
||||
);
|
||||
|
||||
async fn test_channel_message_retrieval(db: &Arc<Database>) {
|
||||
let user = db
|
||||
.create_user(
|
||||
"user@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
db.join_channel_chat(channel, rpc::ConnectionId { owner_id, id: 0 }, user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut all_messages = Vec::new();
|
||||
for i in 0..10 {
|
||||
all_messages.push(
|
||||
db.create_channel_message(channel, user, &i.to_string(), OffsetDateTime::now_utc(), i)
|
||||
.await
|
||||
.unwrap()
|
||||
.0
|
||||
.to_proto(),
|
||||
);
|
||||
}
|
||||
|
||||
let messages = db
|
||||
.get_channel_messages(channel, user, 3, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|message| message.id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(messages, &all_messages[7..10]);
|
||||
|
||||
let messages = db
|
||||
.get_channel_messages(
|
||||
channel,
|
||||
user,
|
||||
4,
|
||||
Some(MessageId::from_proto(all_messages[6])),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|message| message.id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(messages, &all_messages[2..6]);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_message_nonces,
|
||||
test_channel_message_nonces_postgres,
|
||||
@@ -25,10 +87,7 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let channel = db
|
||||
.create_channel("channel", None, "room", user)
|
||||
.await
|
||||
.unwrap();
|
||||
let channel = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
|
||||
@@ -57,3 +116,182 @@ async fn test_channel_message_nonces(db: &Arc<Database>) {
|
||||
assert_eq!(msg1_id, msg3_id);
|
||||
assert_eq!(msg2_id, msg4_id);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_message_new_notification,
|
||||
test_channel_message_new_notification_postgres,
|
||||
test_channel_message_new_notification_sqlite
|
||||
);
|
||||
|
||||
async fn test_channel_message_new_notification(db: &Arc<Database>) {
|
||||
let user = db
|
||||
.create_user(
|
||||
"user_a@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user_a".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
let observer = db
|
||||
.create_user(
|
||||
"user_b@example.com",
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user_b".into(),
|
||||
github_user_id: 1,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
|
||||
|
||||
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_1, observer, user, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.respond_to_channel_invite(channel_1, observer, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.invite_channel_member(channel_2, observer, user, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.respond_to_channel_invite(channel_2, observer, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let owner_id = db.create_server("test").await.unwrap().0 as u32;
|
||||
let user_connection_id = rpc::ConnectionId { owner_id, id: 0 };
|
||||
|
||||
db.join_channel_chat(channel_1, user_connection_id, user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let _ = db
|
||||
.create_channel_message(channel_1, user, "1_1", OffsetDateTime::now_utc(), 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (second_message, _, _) = db
|
||||
.create_channel_message(channel_1, user, "1_2", OffsetDateTime::now_utc(), 2)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (third_message, _, _) = db
|
||||
.create_channel_message(channel_1, user, "1_3", OffsetDateTime::now_utc(), 3)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
db.join_channel_chat(channel_2, user_connection_id, user)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (fourth_message, _, _) = db
|
||||
.create_channel_message(channel_2, user, "2_1", OffsetDateTime::now_utc(), 4)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check that observer has new messages
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_1.to_proto(),
|
||||
message_id: third_message.to_proto(),
|
||||
},
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe the second message
|
||||
db.observe_channel_message(channel_1, observer, second_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer still has a new message
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_1.to_proto(),
|
||||
message_id: third_message.to_proto(),
|
||||
},
|
||||
rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Observe the third message,
|
||||
db.observe_channel_message(channel_1, observer, third_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer does not have a new method
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
}]
|
||||
);
|
||||
|
||||
// Observe the second message again, should not regress our observed state
|
||||
db.observe_channel_message(channel_1, observer, second_message)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Make sure the observer does not have a new message
|
||||
let unseen_messages = db
|
||||
.transaction(|tx| async move {
|
||||
db.unseen_channel_messages(observer, &[channel_1, channel_2], &*tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
unseen_messages,
|
||||
[rpc::proto::UnseenChannelMessage {
|
||||
channel_id: channel_2.to_proto(),
|
||||
message_id: fourth_message.to_proto(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ mod connection_pool;
|
||||
use crate::{
|
||||
auth,
|
||||
db::{
|
||||
self, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId, ServerId, User,
|
||||
UserId,
|
||||
self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
|
||||
ServerId, User, UserId,
|
||||
},
|
||||
executor::Executor,
|
||||
AppState, Result,
|
||||
@@ -38,8 +38,8 @@ use lazy_static::lazy_static;
|
||||
use prometheus::{register_int_gauge, IntGauge};
|
||||
use rpc::{
|
||||
proto::{
|
||||
self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, ChannelEdge, EntityMessage,
|
||||
EnvelopedMessage, LiveKitConnectionInfo, RequestMessage,
|
||||
self, Ack, AnyTypedEnvelope, ChannelEdge, EntityMessage, EnvelopedMessage,
|
||||
LiveKitConnectionInfo, RequestMessage, UpdateChannelBufferCollaborators,
|
||||
},
|
||||
Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||
};
|
||||
@@ -63,6 +63,7 @@ use time::OffsetDateTime;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{info_span, instrument, Instrument};
|
||||
use util::channel::RELEASE_CHANNEL_NAME;
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
@@ -274,7 +275,9 @@ impl Server {
|
||||
.add_message_handler(unfollow)
|
||||
.add_message_handler(update_followers)
|
||||
.add_message_handler(update_diff_base)
|
||||
.add_request_handler(get_private_user_info);
|
||||
.add_request_handler(get_private_user_info)
|
||||
.add_message_handler(acknowledge_channel_message)
|
||||
.add_message_handler(acknowledge_buffer_version);
|
||||
|
||||
Arc::new(server)
|
||||
}
|
||||
@@ -313,9 +316,16 @@ impl Server {
|
||||
.trace_err()
|
||||
{
|
||||
for connection_id in refreshed_channel_buffer.connection_ids {
|
||||
for message in &refreshed_channel_buffer.removed_collaborators {
|
||||
peer.send(connection_id, message.clone()).trace_err();
|
||||
}
|
||||
peer.send(
|
||||
connection_id,
|
||||
proto::UpdateChannelBufferCollaborators {
|
||||
channel_id: channel_id.to_proto(),
|
||||
collaborators: refreshed_channel_buffer
|
||||
.collaborators
|
||||
.clone(),
|
||||
},
|
||||
)
|
||||
.trace_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -928,11 +938,6 @@ async fn create_room(
|
||||
util::async_iife!({
|
||||
let live_kit = live_kit?;
|
||||
|
||||
live_kit
|
||||
.create_room(live_kit_room.clone())
|
||||
.await
|
||||
.trace_err()?;
|
||||
|
||||
let token = live_kit
|
||||
.room_token(&live_kit_room, &session.user_id.to_string())
|
||||
.trace_err()?;
|
||||
@@ -948,7 +953,12 @@ async fn create_room(
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.create_room(session.user_id, session.connection_id, &live_kit_room)
|
||||
.create_room(
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
&live_kit_room,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CreateRoomResponse {
|
||||
@@ -970,7 +980,12 @@ async fn join_room(
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
.join_room(
|
||||
room_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
room_updated(&room.room, &session.peer);
|
||||
room.into_inner()
|
||||
@@ -1883,94 +1898,94 @@ async fn follow(
|
||||
response: Response<proto::Follow>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let room_id = RoomId::from_proto(request.room_id);
|
||||
let project_id = request.project_id.map(ProjectId::from_proto);
|
||||
let leader_id = request
|
||||
.leader_id
|
||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||
.into();
|
||||
let follower_id = session.connection_id;
|
||||
|
||||
{
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.check_room_participants(room_id, leader_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
if !project_connection_ids.contains(&leader_id) {
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
}
|
||||
|
||||
let mut response_payload = session
|
||||
let response_payload = session
|
||||
.peer
|
||||
.forward_request(session.connection_id, leader_id, request)
|
||||
.await?;
|
||||
response_payload
|
||||
.views
|
||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
||||
response.send(response_payload)?;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.follow(project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
if let Some(project_id) = project_id {
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.follow(room_id, project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let room_id = RoomId::from_proto(request.room_id);
|
||||
let project_id = request.project_id.map(ProjectId::from_proto);
|
||||
let leader_id = request
|
||||
.leader_id
|
||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||
.into();
|
||||
let follower_id = session.connection_id;
|
||||
|
||||
if !session
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?
|
||||
.contains(&leader_id)
|
||||
{
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
.check_room_participants(room_id, leader_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, leader_id, request)?;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.unfollow(project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
if let Some(project_id) = project_id {
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.unfollow(room_id, project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_followers(request: proto::UpdateFollowers, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let project_connection_ids = session
|
||||
.db
|
||||
.lock()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
let room_id = RoomId::from_proto(request.room_id);
|
||||
let database = session.db.lock().await;
|
||||
|
||||
let leader_id = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::CreateView(payload) => payload.leader_id,
|
||||
let connection_ids = if let Some(project_id) = request.project_id {
|
||||
let project_id = ProjectId::from_proto(project_id);
|
||||
database
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?
|
||||
} else {
|
||||
database
|
||||
.room_connection_ids(room_id, session.connection_id)
|
||||
.await?
|
||||
};
|
||||
|
||||
// For now, don't send view update messages back to that view's current leader.
|
||||
let connection_id_to_omit = request.variant.as_ref().and_then(|variant| match variant {
|
||||
proto::update_followers::Variant::UpdateView(payload) => payload.leader_id,
|
||||
proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id,
|
||||
_ => None,
|
||||
});
|
||||
|
||||
for follower_peer_id in request.follower_ids.iter().copied() {
|
||||
let follower_connection_id = follower_peer_id.into();
|
||||
if project_connection_ids.contains(&follower_connection_id)
|
||||
&& Some(follower_peer_id) != leader_id
|
||||
if Some(follower_peer_id) != connection_id_to_omit
|
||||
&& connection_ids.contains(&follower_connection_id)
|
||||
{
|
||||
session.peer.forward_send(
|
||||
session.connection_id,
|
||||
@@ -2186,15 +2201,10 @@ async fn create_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let db = session.db().await;
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
|
||||
if let Some(live_kit) = session.live_kit_client.as_ref() {
|
||||
live_kit.create_room(live_kit_room.clone()).await?;
|
||||
}
|
||||
|
||||
let parent_id = request.parent_id.map(|id| ChannelId::from_proto(id));
|
||||
let id = db
|
||||
.create_channel(&request.name, parent_id, &live_kit_room, session.user_id)
|
||||
.create_channel(&request.name, parent_id, session.user_id)
|
||||
.await?;
|
||||
|
||||
let channel = proto::Channel {
|
||||
@@ -2474,6 +2484,11 @@ async fn move_channel(
|
||||
.move_channel(session.user_id, channel_id, from_parent, to)
|
||||
.await?;
|
||||
|
||||
if channels_to_send.is_empty() {
|
||||
response.send(Ack {})?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let members_from = db.get_channel_members(from_parent).await?;
|
||||
let members_to = db.get_channel_members(to).await?;
|
||||
|
||||
@@ -2556,6 +2571,8 @@ async fn respond_to_channel_invite(
|
||||
name: channel.name,
|
||||
}),
|
||||
);
|
||||
update.unseen_channel_messages = result.channel_messages;
|
||||
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
||||
update.insert_edge = result.channels.edges;
|
||||
update
|
||||
.channel_participants
|
||||
@@ -2592,15 +2609,23 @@ async fn join_channel(
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
||||
|
||||
let joined_room = {
|
||||
leave_room_for_session(&session).await?;
|
||||
let db = session.db().await;
|
||||
|
||||
let room_id = db.room_id_for_channel(channel_id).await?;
|
||||
let room_id = db
|
||||
.get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
|
||||
.await?;
|
||||
|
||||
let joined_room = db
|
||||
.join_room(room_id, session.user_id, session.connection_id)
|
||||
.join_room(
|
||||
room_id,
|
||||
session.user_id,
|
||||
session.connection_id,
|
||||
RELEASE_CHANNEL_NAME.as_str(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let live_kit_connection_info = session.live_kit_client.as_ref().and_then(|live_kit| {
|
||||
@@ -2653,18 +2678,12 @@ async fn join_channel_buffer(
|
||||
.join_channel_buffer(channel_id, session.user_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
let replica_id = open_response.replica_id;
|
||||
let collaborators = open_response.collaborators.clone();
|
||||
|
||||
response.send(open_response)?;
|
||||
|
||||
let update = AddChannelBufferCollaborator {
|
||||
let update = UpdateChannelBufferCollaborators {
|
||||
channel_id: channel_id.to_proto(),
|
||||
collaborator: Some(proto::Collaborator {
|
||||
user_id: session.user_id.to_proto(),
|
||||
peer_id: Some(session.connection_id.into()),
|
||||
replica_id,
|
||||
}),
|
||||
collaborators: collaborators.clone(),
|
||||
};
|
||||
channel_buffer_updated(
|
||||
session.connection_id,
|
||||
@@ -2685,7 +2704,7 @@ async fn update_channel_buffer(
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
|
||||
let collaborators = db
|
||||
let (collaborators, non_collaborators, epoch, version) = db
|
||||
.update_channel_buffer(channel_id, session.user_id, &request.operations)
|
||||
.await?;
|
||||
|
||||
@@ -2698,6 +2717,29 @@ async fn update_channel_buffer(
|
||||
},
|
||||
&session.peer,
|
||||
);
|
||||
|
||||
let pool = &*session.connection_pool().await;
|
||||
|
||||
broadcast(
|
||||
None,
|
||||
non_collaborators
|
||||
.iter()
|
||||
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|
||||
|peer_id| {
|
||||
session.peer.send(
|
||||
peer_id.into(),
|
||||
proto::UpdateChannels {
|
||||
unseen_channel_buffer_changes: vec![proto::UnseenChannelBufferChange {
|
||||
channel_id: channel_id.to_proto(),
|
||||
epoch: epoch as u64,
|
||||
version: version.clone(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2711,8 +2753,8 @@ async fn rejoin_channel_buffers(
|
||||
.rejoin_channel_buffers(&request.buffers, session.user_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
for buffer in &buffers {
|
||||
let collaborators_to_notify = buffer
|
||||
for rejoined_buffer in &buffers {
|
||||
let collaborators_to_notify = rejoined_buffer
|
||||
.buffer
|
||||
.collaborators
|
||||
.iter()
|
||||
@@ -2720,10 +2762,9 @@ async fn rejoin_channel_buffers(
|
||||
channel_buffer_updated(
|
||||
session.connection_id,
|
||||
collaborators_to_notify,
|
||||
&proto::UpdateChannelBufferCollaborator {
|
||||
channel_id: buffer.buffer.channel_id,
|
||||
old_peer_id: Some(buffer.old_connection_id.into()),
|
||||
new_peer_id: Some(session.connection_id.into()),
|
||||
&proto::UpdateChannelBufferCollaborators {
|
||||
channel_id: rejoined_buffer.buffer.channel_id,
|
||||
collaborators: rejoined_buffer.buffer.collaborators.clone(),
|
||||
},
|
||||
&session.peer,
|
||||
);
|
||||
@@ -2744,7 +2785,7 @@ async fn leave_channel_buffer(
|
||||
let db = session.db().await;
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
|
||||
let collaborators_to_notify = db
|
||||
let left_buffer = db
|
||||
.leave_channel_buffer(channel_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
@@ -2752,10 +2793,10 @@ async fn leave_channel_buffer(
|
||||
|
||||
channel_buffer_updated(
|
||||
session.connection_id,
|
||||
collaborators_to_notify,
|
||||
&proto::RemoveChannelBufferCollaborator {
|
||||
left_buffer.connections,
|
||||
&proto::UpdateChannelBufferCollaborators {
|
||||
channel_id: channel_id.to_proto(),
|
||||
peer_id: Some(session.connection_id.into()),
|
||||
collaborators: left_buffer.collaborators,
|
||||
},
|
||||
&session.peer,
|
||||
);
|
||||
@@ -2794,7 +2835,7 @@ async fn send_channel_message(
|
||||
.ok_or_else(|| anyhow!("nonce can't be blank"))?;
|
||||
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let (message_id, connection_ids) = session
|
||||
let (message_id, connection_ids, non_participants) = session
|
||||
.db()
|
||||
.await
|
||||
.create_channel_message(
|
||||
@@ -2824,6 +2865,27 @@ async fn send_channel_message(
|
||||
response.send(proto::SendChannelMessageResponse {
|
||||
message: Some(message),
|
||||
})?;
|
||||
|
||||
let pool = &*session.connection_pool().await;
|
||||
broadcast(
|
||||
None,
|
||||
non_participants
|
||||
.iter()
|
||||
.flat_map(|user_id| pool.user_connection_ids(*user_id)),
|
||||
|peer_id| {
|
||||
session.peer.send(
|
||||
peer_id.into(),
|
||||
proto::UpdateChannels {
|
||||
unseen_channel_messages: vec![proto::UnseenChannelMessage {
|
||||
channel_id: channel_id.to_proto(),
|
||||
message_id: message_id.to_proto(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -2846,6 +2908,38 @@ async fn remove_channel_message(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn acknowledge_channel_message(
|
||||
request: proto::AckChannelMessage,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let message_id = MessageId::from_proto(request.message_id);
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.observe_channel_message(channel_id, session.user_id, message_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn acknowledge_buffer_version(
|
||||
request: proto::AckBufferOperation,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let buffer_id = BufferId::from_proto(request.buffer_id);
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.observe_buffer_version(
|
||||
buffer_id,
|
||||
session.user_id,
|
||||
request.epoch as i32,
|
||||
&request.version,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn join_channel_chat(
|
||||
request: proto::JoinChannelChat,
|
||||
response: Response<proto::JoinChannelChat>,
|
||||
@@ -2981,6 +3075,8 @@ fn build_initial_channels_update(
|
||||
});
|
||||
}
|
||||
|
||||
update.unseen_channel_buffer_changes = channels.unseen_buffer_changes;
|
||||
update.unseen_channel_messages = channels.channel_messages;
|
||||
update.insert_edge = channels.channels.edges;
|
||||
|
||||
for (channel_id, participants) in channels.channel_participants {
|
||||
@@ -3230,13 +3326,13 @@ async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> {
|
||||
.leave_channel_buffers(session.connection_id)
|
||||
.await?;
|
||||
|
||||
for (channel_id, connections) in left_channel_buffers {
|
||||
for left_buffer in left_channel_buffers {
|
||||
channel_buffer_updated(
|
||||
session.connection_id,
|
||||
connections,
|
||||
&proto::RemoveChannelBufferCollaborator {
|
||||
channel_id: channel_id.to_proto(),
|
||||
peer_id: Some(session.connection_id.into()),
|
||||
left_buffer.connections,
|
||||
&proto::UpdateChannelBufferCollaborators {
|
||||
channel_id: left_buffer.channel_id.to_proto(),
|
||||
collaborators: left_buffer.collaborators,
|
||||
},
|
||||
&session.peer,
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ use gpui::{ModelHandle, TestAppContext};
|
||||
mod channel_buffer_tests;
|
||||
mod channel_message_tests;
|
||||
mod channel_tests;
|
||||
mod following_tests;
|
||||
mod integration_tests;
|
||||
mod random_channel_buffer_tests;
|
||||
mod random_project_collaboration_tests;
|
||||
|
||||
@@ -3,15 +3,17 @@ use crate::{
|
||||
tests::TestServer,
|
||||
};
|
||||
use call::ActiveCall;
|
||||
use channel::Channel;
|
||||
use client::UserId;
|
||||
use channel::{Channel, ACKNOWLEDGE_DEBOUNCE_INTERVAL};
|
||||
use client::ParticipantIndex;
|
||||
use client::{Collaborator, UserId};
|
||||
use collab_ui::channel_view::ChannelView;
|
||||
use collections::HashMap;
|
||||
use editor::{Anchor, Editor, ToOffset};
|
||||
use futures::future;
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||
use rpc::{proto, RECEIVE_TIMEOUT};
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_core_channel_buffers(
|
||||
@@ -100,7 +102,7 @@ async fn test_core_channel_buffers(
|
||||
channel_buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_collaborators(
|
||||
&buffer.collaborators(),
|
||||
&[client_b.user_id(), client_a.user_id()],
|
||||
&[client_a.user_id(), client_b.user_id()],
|
||||
);
|
||||
});
|
||||
|
||||
@@ -120,10 +122,10 @@ async fn test_core_channel_buffers(
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_buffer_replica_ids(
|
||||
async fn test_channel_notes_participant_indices(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
mut cx_a: &mut TestAppContext,
|
||||
mut cx_b: &mut TestAppContext,
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
@@ -132,6 +134,13 @@ async fn test_channel_buffer_replica_ids(
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").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);
|
||||
cx_c.update(editor::init);
|
||||
|
||||
let channel_id = server
|
||||
.make_channel(
|
||||
"the-channel",
|
||||
@@ -141,140 +150,173 @@ async fn test_channel_buffer_replica_ids(
|
||||
)
|
||||
.await;
|
||||
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
let active_call_c = cx_c.read(ActiveCall::global);
|
||||
|
||||
// Clients A and B join a channel.
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Clients A, B, and C join a channel buffer
|
||||
// C first so that the replica IDs in the project and the channel buffer are different
|
||||
let channel_buffer_c = client_c
|
||||
.channel_store()
|
||||
.update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_buffer_b = client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_buffer_a = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client B shares a project
|
||||
client_b
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree("/dir", json!({ "file.txt": "contents" }))
|
||||
.insert_tree("/root", json!({"file.txt": "123"}))
|
||||
.await;
|
||||
let (project_b, _) = client_b.build_local_project("/dir", cx_b).await;
|
||||
let shared_project_id = active_call_b
|
||||
.update(cx_b, |call, cx| call.share_project(project_b.clone(), cx))
|
||||
let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await;
|
||||
let project_b = client_b.build_empty_local_project(cx_b);
|
||||
let project_c = client_c.build_empty_local_project(cx_c);
|
||||
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_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
|
||||
|
||||
// Clients A, B, and C open the channel notes
|
||||
let channel_view_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_b = cx_b
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let channel_view_c = cx_c
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A joins the project
|
||||
let project_a = client_a.build_remote_project(shared_project_id, cx_a).await;
|
||||
// Clients A, B, and C all insert and select some text
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.insert("a", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Client C is in a separate project.
|
||||
client_c.fs().insert_tree("/dir", json!({})).await;
|
||||
let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await;
|
||||
|
||||
// Note that each user has a different replica id in the projects vs the
|
||||
// channel buffer.
|
||||
channel_buffer_a.read_with(cx_a, |channel_buffer, cx| {
|
||||
assert_eq!(project_a.read(cx).replica_id(), 1);
|
||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2);
|
||||
channel_view_b.update(cx_b, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
editor.insert("b", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![1..2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
channel_buffer_b.read_with(cx_b, |channel_buffer, cx| {
|
||||
assert_eq!(project_b.read(cx).replica_id(), 0);
|
||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1);
|
||||
});
|
||||
channel_buffer_c.read_with(cx_c, |channel_buffer, cx| {
|
||||
// C is not in the project
|
||||
assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0);
|
||||
deterministic.run_until_parked();
|
||||
channel_view_c.update(cx_c, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.move_down(&Default::default(), cx);
|
||||
editor.insert("c", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let channel_window_a =
|
||||
cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx));
|
||||
let channel_window_b =
|
||||
cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx));
|
||||
let channel_window_c = cx_c.add_window(|cx| {
|
||||
ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx)
|
||||
// Client A sees clients B and C without assigned colors, because they aren't
|
||||
// in a call together.
|
||||
deterministic.run_until_parked();
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx);
|
||||
});
|
||||
});
|
||||
|
||||
let channel_view_a = channel_window_a.root(cx_a);
|
||||
let channel_view_b = channel_window_b.root(cx_b);
|
||||
let channel_view_c = channel_window_c.root(cx_c);
|
||||
// Clients A and B join the same call.
|
||||
for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] {
|
||||
call.update(*cx, |call, cx| call.join_channel(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// For clients A and B, the replica ids in the channel buffer are mapped
|
||||
// so that they match the same users' replica ids in their shared project.
|
||||
channel_view_a.read_with(cx_a, |view, cx| {
|
||||
assert_eq!(
|
||||
view.editor.read(cx).replica_id_map().unwrap(),
|
||||
&[(1, 0), (2, 1)].into_iter().collect::<HashMap<_, _>>()
|
||||
);
|
||||
// Clients A and B see each other with two different assigned colors. Client C
|
||||
// still doesn't have a color.
|
||||
deterministic.run_until_parked();
|
||||
channel_view_a.update(cx_a, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
assert_remote_selections(
|
||||
editor,
|
||||
&[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
channel_view_b.read_with(cx_b, |view, cx| {
|
||||
assert_eq!(
|
||||
view.editor.read(cx).replica_id_map().unwrap(),
|
||||
&[(1, 0), (2, 1)].into_iter().collect::<HashMap<u16, u16>>(),
|
||||
)
|
||||
channel_view_b.update(cx_b, |notes, cx| {
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
assert_remote_selections(
|
||||
editor,
|
||||
&[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)],
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Client C only sees themself, as they're not part of any shared project
|
||||
channel_view_c.read_with(cx_c, |view, cx| {
|
||||
assert_eq!(
|
||||
view.editor.read(cx).replica_id_map().unwrap(),
|
||||
&[(0, 0)].into_iter().collect::<HashMap<u16, u16>>(),
|
||||
);
|
||||
});
|
||||
|
||||
// Client C joins the project that clients A and B are in.
|
||||
active_call_c
|
||||
.update(cx_c, |call, cx| call.join_channel(channel_id, cx))
|
||||
// Client A shares a project, and client B joins.
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_c = client_c.build_remote_project(shared_project_id, cx_c).await;
|
||||
deterministic.run_until_parked();
|
||||
project_c.read_with(cx_c, |project, _| {
|
||||
assert_eq!(project.replica_id(), 2);
|
||||
});
|
||||
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);
|
||||
|
||||
// For clients A and B, client C's replica id in the channel buffer is
|
||||
// now mapped to their replica id in the shared project.
|
||||
channel_view_a.read_with(cx_a, |view, cx| {
|
||||
assert_eq!(
|
||||
view.editor.read(cx).replica_id_map().unwrap(),
|
||||
&[(1, 0), (2, 1), (0, 2)]
|
||||
.into_iter()
|
||||
.collect::<HashMap<_, _>>()
|
||||
);
|
||||
// Clients A and B open the same file.
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id_a, "file.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![0..1]);
|
||||
});
|
||||
});
|
||||
channel_view_b.read_with(cx_b, |view, cx| {
|
||||
assert_eq!(
|
||||
view.editor.read(cx).replica_id_map().unwrap(),
|
||||
&[(1, 0), (2, 1), (0, 2)]
|
||||
.into_iter()
|
||||
.collect::<HashMap<_, _>>(),
|
||||
)
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![2..3]);
|
||||
});
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Clients A and B see each other with the same colors as in the channel notes.
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_remote_selections(
|
||||
editor: &mut Editor,
|
||||
expected_selections: &[(Option<ParticipantIndex>, Range<usize>)],
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
let range = Anchor::min()..Anchor::max();
|
||||
let remote_selections = snapshot
|
||||
.remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx)
|
||||
.map(|s| {
|
||||
let start = s.selection.start.to_offset(&snapshot.buffer_snapshot);
|
||||
let end = s.selection.end.to_offset(&snapshot.buffer_snapshot);
|
||||
(s.participant_index, start..end)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
remote_selections, expected_selections,
|
||||
"incorrect remote selections"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reopen_channel_buffer(deterministic: Arc<Deterministic>, cx_a: &mut TestAppContext) {
|
||||
async fn test_multiple_handles_to_channel_buffer(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
@@ -368,10 +410,7 @@ async fn test_channel_buffer_disconnect(
|
||||
channel_buffer_a.update(cx_a, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.channel().as_ref(),
|
||||
&Channel {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string()
|
||||
}
|
||||
&channel(channel_id, "the-channel")
|
||||
);
|
||||
assert!(!buffer.is_connected());
|
||||
});
|
||||
@@ -396,15 +435,21 @@ async fn test_channel_buffer_disconnect(
|
||||
channel_buffer_b.update(cx_b, |buffer, _| {
|
||||
assert_eq!(
|
||||
buffer.channel().as_ref(),
|
||||
&Channel {
|
||||
id: channel_id,
|
||||
name: "the-channel".to_string()
|
||||
}
|
||||
&channel(channel_id, "the-channel")
|
||||
);
|
||||
assert!(!buffer.is_connected());
|
||||
});
|
||||
}
|
||||
|
||||
fn channel(id: u64, name: &'static str) -> Channel {
|
||||
Channel {
|
||||
id,
|
||||
name: name.to_string(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rejoin_channel_buffer(
|
||||
deterministic: Arc<Deterministic>,
|
||||
@@ -565,26 +610,284 @@ async fn test_channel_buffers_and_server_restarts(
|
||||
|
||||
channel_buffer_a.read_with(cx_a, |buffer_a, _| {
|
||||
channel_buffer_b.read_with(cx_b, |buffer_b, _| {
|
||||
assert_eq!(
|
||||
buffer_a
|
||||
.collaborators()
|
||||
.iter()
|
||||
.map(|c| c.user_id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![client_a.user_id().unwrap(), client_b.user_id().unwrap()]
|
||||
assert_collaborators(
|
||||
buffer_a.collaborators(),
|
||||
&[client_a.user_id(), client_b.user_id()],
|
||||
);
|
||||
assert_eq!(buffer_a.collaborators(), buffer_b.collaborators());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_following_to_channel_notes_without_a_shared_project(
|
||||
deterministic: Arc<Deterministic>,
|
||||
mut cx_a: &mut TestAppContext,
|
||||
mut cx_b: &mut TestAppContext,
|
||||
mut cx_c: &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;
|
||||
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
cx_c.update(editor::init);
|
||||
cx_a.update(collab_ui::channel_view::init);
|
||||
cx_b.update(collab_ui::channel_view::init);
|
||||
cx_c.update(collab_ui::channel_view::init);
|
||||
|
||||
let channel_1_id = server
|
||||
.make_channel(
|
||||
"channel-1",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||
)
|
||||
.await;
|
||||
let channel_2_id = server
|
||||
.make_channel(
|
||||
"channel-2",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&mut [(&client_b, cx_b), (&client_c, cx_c)],
|
||||
)
|
||||
.await;
|
||||
|
||||
// Clients A, B, and C join a channel.
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
let active_call_c = cx_c.read(ActiveCall::global);
|
||||
for (call, cx) in [
|
||||
(&active_call_a, &mut cx_a),
|
||||
(&active_call_b, &mut cx_b),
|
||||
(&active_call_c, &mut cx_c),
|
||||
] {
|
||||
call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
deterministic.run_until_parked();
|
||||
|
||||
// Clients A, B, and C all open their own unshared projects.
|
||||
client_a.fs().insert_tree("/a", json!({})).await;
|
||||
client_b.fs().insert_tree("/b", json!({})).await;
|
||||
client_c.fs().insert_tree("/c", json!({})).await;
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
let (project_b, _) = client_b.build_local_project("/b", cx_b).await;
|
||||
let (project_c, _) = client_b.build_local_project("/c", cx_c).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_c = client_c.build_workspace(&project_c, cx_c).root(cx_c);
|
||||
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A opens the notes for channel 1.
|
||||
let channel_view_1_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
channel_view_1_a.update(cx_a, |notes, cx| {
|
||||
assert_eq!(notes.channel(cx).name, "channel-1");
|
||||
notes.editor.update(cx, |editor, cx| {
|
||||
editor.insert("Hello from A.", cx);
|
||||
editor.change_selections(None, cx, |selections| {
|
||||
selections.select_ranges(vec![3..4]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Client B follows client A.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(client_a.peer_id().unwrap(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client B is taken to the notes for channel 1, with the same
|
||||
// text selected as client A.
|
||||
deterministic.run_until_parked();
|
||||
let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.leader_for_pane(workspace.active_pane()),
|
||||
Some(client_a.peer_id().unwrap())
|
||||
);
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.expect("no active item")
|
||||
.downcast::<ChannelView>()
|
||||
.expect("active item is not a channel view")
|
||||
});
|
||||
channel_view_1_b.read_with(cx_b, |notes, cx| {
|
||||
assert_eq!(notes.channel(cx).name, "channel-1");
|
||||
let editor = notes.editor.read(cx);
|
||||
assert_eq!(editor.text(cx), "Hello from A.");
|
||||
assert_eq!(editor.selections.ranges::<usize>(cx), &[3..4]);
|
||||
});
|
||||
|
||||
// Client A opens the notes for channel 2.
|
||||
let channel_view_2_a = cx_a
|
||||
.update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
channel_view_2_a.read_with(cx_a, |notes, cx| {
|
||||
assert_eq!(notes.channel(cx).name, "channel-2");
|
||||
});
|
||||
|
||||
// Client B is taken to the notes for channel 2.
|
||||
deterministic.run_until_parked();
|
||||
let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
assert_eq!(
|
||||
workspace.leader_for_pane(workspace.active_pane()),
|
||||
Some(client_a.peer_id().unwrap())
|
||||
);
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.expect("no active item")
|
||||
.downcast::<ChannelView>()
|
||||
.expect("active item is not a channel view")
|
||||
});
|
||||
channel_view_2_b.read_with(cx_b, |notes, cx| {
|
||||
assert_eq!(notes.channel(cx).name, "channel-2");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_buffer_changes(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
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;
|
||||
|
||||
let channel_id = server
|
||||
.make_channel(
|
||||
"the-channel",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&mut [(&client_b, cx_b)],
|
||||
)
|
||||
.await;
|
||||
|
||||
let channel_buffer_a = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A makes an edit, and client B should see that the note has changed.
|
||||
channel_buffer_a.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "1")], None, cx);
|
||||
})
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let has_buffer_changed = cx_b.read(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(has_buffer_changed);
|
||||
|
||||
// Opening the buffer should clear the changed flag.
|
||||
let project_b = client_b.build_empty_local_project(cx_b);
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
let channel_view_b = cx_b
|
||||
.update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let has_buffer_changed = cx_b.read(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
// Editing the channel while the buffer is open should not show that the buffer has changed.
|
||||
channel_buffer_a.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "2")], None, cx);
|
||||
})
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let has_buffer_changed = cx_b.read(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL);
|
||||
|
||||
// Test that the server is tracking things correctly, and we retain our 'not changed'
|
||||
// state across a disconnect
|
||||
server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic);
|
||||
let has_buffer_changed = cx_b.read(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(!has_buffer_changed);
|
||||
|
||||
// Closing the buffer should re-enable change tracking
|
||||
cx_b.update(|cx| {
|
||||
workspace_b.update(cx, |workspace, cx| {
|
||||
workspace.close_all_items_and_panes(&Default::default(), cx)
|
||||
});
|
||||
|
||||
drop(channel_view_b)
|
||||
});
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
channel_buffer_a.update(cx_a, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "3")], None, cx);
|
||||
})
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let has_buffer_changed = cx_b.read(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_channel_buffer_changed(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
assert!(has_buffer_changed);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option<UserId>]) {
|
||||
fn assert_collaborators(collaborators: &HashMap<PeerId, Collaborator>, ids: &[Option<UserId>]) {
|
||||
let mut user_ids = collaborators
|
||||
.values()
|
||||
.map(|collaborator| collaborator.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
user_ids.sort();
|
||||
assert_eq!(
|
||||
collaborators
|
||||
.into_iter()
|
||||
.map(|collaborator| collaborator.user_id)
|
||||
.collect::<Vec<_>>(),
|
||||
user_ids,
|
||||
ids.into_iter().map(|id| id.unwrap()).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer};
|
||||
use channel::{ChannelChat, ChannelMessageId};
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||
use collab_ui::chat_panel::ChatPanel;
|
||||
use gpui::{executor::Deterministic, BorrowAppContext, ModelHandle, TestAppContext};
|
||||
use std::sync::Arc;
|
||||
use workspace::dock::Panel;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_basic_channel_messages(
|
||||
@@ -223,3 +225,136 @@ fn assert_messages(chat: &ModelHandle<ChannelChat>, messages: &[&str], cx: &mut
|
||||
messages
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_message_changes(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
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;
|
||||
|
||||
let channel_id = server
|
||||
.make_channel(
|
||||
"the-channel",
|
||||
None,
|
||||
(&client_a, cx_a),
|
||||
&mut [(&client_b, cx_b)],
|
||||
)
|
||||
.await;
|
||||
|
||||
// Client A sends a message, client B should see that there is a new message.
|
||||
let channel_chat_a = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |store, cx| store.open_channel_chat(channel_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("one".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.read_with(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
|
||||
// Opening the chat should clear the changed flag.
|
||||
cx_b.update(|cx| {
|
||||
collab_ui::init(&client_b.app_state, cx);
|
||||
});
|
||||
let project_b = client_b.build_empty_local_project(cx_b);
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
let chat_panel_b = workspace_b.update(cx_b, |workspace, cx| ChatPanel::new(workspace, cx));
|
||||
chat_panel_b
|
||||
.update(cx_b, |chat_panel, cx| {
|
||||
chat_panel.set_active(true, cx);
|
||||
chat_panel.select_channel(channel_id, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.read_with(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(!b_has_messages);
|
||||
|
||||
// Sending a message while the chat is open should not change the flag.
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("two".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.read_with(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(!b_has_messages);
|
||||
|
||||
// Sending a message while the chat is closed should change the flag.
|
||||
chat_panel_b.update(cx_b, |chat_panel, cx| {
|
||||
chat_panel.set_active(false, cx);
|
||||
});
|
||||
|
||||
// Sending a message while the chat is open should not change the flag.
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("three".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.read_with(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
|
||||
// Closing the chat should re-enable change tracking
|
||||
cx_b.update(|_| drop(chat_panel_b));
|
||||
|
||||
channel_chat_a
|
||||
.update(cx_a, |c, cx| c.send_message("four".into(), cx).unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
deterministic.run_until_parked();
|
||||
|
||||
let b_has_messages = cx_b.read_with(|cx| {
|
||||
client_b
|
||||
.channel_store()
|
||||
.read(cx)
|
||||
.has_new_messages(channel_id)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
assert!(b_has_messages);
|
||||
}
|
||||
|
||||
@@ -145,8 +145,6 @@ async fn test_core_channels(
|
||||
],
|
||||
);
|
||||
|
||||
println!("STARTING CREATE CHANNEL C");
|
||||
|
||||
let channel_c_id = client_a
|
||||
.channel_store()
|
||||
.update(cx_a, |channel_store, cx| {
|
||||
@@ -382,6 +380,8 @@ async fn test_channel_room(
|
||||
|
||||
// Give everyone a chance to observe user A joining
|
||||
deterministic.run_until_parked();
|
||||
let room_a = active_call_a.read_with(cx_a, |call, _| call.room().unwrap().clone());
|
||||
room_a.read_with(cx_a, |room, _| assert!(room.is_connected()));
|
||||
|
||||
client_a.channel_store().read_with(cx_a, |channels, _| {
|
||||
assert_participants_eq(
|
||||
@@ -1028,10 +1028,6 @@ async fn test_channel_moving(
|
||||
// - ep
|
||||
assert_channels_list_shape(client_c.channel_store(), cx_c, &[(channel_ep_id, 0)]);
|
||||
|
||||
println!("*******************************************");
|
||||
println!("********** STARTING LINK CHANNEL **********");
|
||||
println!("*******************************************");
|
||||
dbg!(client_b.user_id());
|
||||
client_b
|
||||
.channel_store()
|
||||
.update(cx_b, |channel_store, cx| {
|
||||
@@ -1199,5 +1195,5 @@ fn assert_channels_list_shape(
|
||||
.map(|(depth, channel)| (channel.id, depth))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(dbg!(actual), expected_channels);
|
||||
pretty_assertions::assert_eq!(actual, expected_channels);
|
||||
}
|
||||
|
||||
1683
crates/collab/src/tests/following_tests.rs
Normal file
1683
crates/collab/src/tests/following_tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -46,12 +46,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
let db = &server.app_state.db;
|
||||
for ix in 0..CHANNEL_COUNT {
|
||||
let id = db
|
||||
.create_channel(
|
||||
&format!("channel-{ix}"),
|
||||
None,
|
||||
&format!("livekit-room-{ix}"),
|
||||
users[0].user_id,
|
||||
)
|
||||
.create_channel(&format!("channel-{ix}"), None, users[0].user_id)
|
||||
.await
|
||||
.unwrap();
|
||||
for user in &users[1..] {
|
||||
@@ -273,7 +268,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
// channel buffer.
|
||||
let collaborators = channel_buffer.collaborators();
|
||||
let mut user_ids =
|
||||
collaborators.iter().map(|c| c.user_id).collect::<Vec<_>>();
|
||||
collaborators.values().map(|c| c.user_id).collect::<Vec<_>>();
|
||||
user_ids.sort();
|
||||
assert_eq!(
|
||||
user_ids,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
db::{tests::TestDb, NewUserParams, UserId},
|
||||
executor::Executor,
|
||||
rpc::{Server, CLEANUP_TIMEOUT},
|
||||
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
|
||||
AppState,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -15,8 +15,10 @@ use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext, WindowHandle};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
@@ -29,7 +31,7 @@ use std::{
|
||||
},
|
||||
};
|
||||
use util::http::FakeHttpClient;
|
||||
use workspace::Workspace;
|
||||
use workspace::{Workspace, WorkspaceStore};
|
||||
|
||||
pub struct TestServer {
|
||||
pub app_state: Arc<AppState>,
|
||||
@@ -43,6 +45,7 @@ pub struct TestServer {
|
||||
pub struct TestClient {
|
||||
pub username: String,
|
||||
pub app_state: Arc<workspace::AppState>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
state: RefCell<TestClientState>,
|
||||
}
|
||||
|
||||
@@ -151,12 +154,12 @@ impl TestServer {
|
||||
|
||||
Arc::get_mut(&mut client)
|
||||
.unwrap()
|
||||
.set_id(user_id.0 as usize)
|
||||
.set_id(user_id.to_proto())
|
||||
.override_authenticate(move |cx| {
|
||||
cx.spawn(|_| async move {
|
||||
let access_token = "the-token".to_string();
|
||||
Ok(Credentials {
|
||||
user_id: user_id.0 as u64,
|
||||
user_id: user_id.to_proto(),
|
||||
access_token,
|
||||
})
|
||||
})
|
||||
@@ -204,17 +207,19 @@ impl TestServer {
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let channel_store =
|
||||
cx.add_model(|cx| ChannelStore::new(client.clone(), user_store.clone(), cx));
|
||||
let workspace_store = cx.add_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let mut language_registry = LanguageRegistry::test();
|
||||
language_registry.set_executor(cx.background());
|
||||
let app_state = Arc::new(workspace::AppState {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
channel_store: channel_store.clone(),
|
||||
languages: Arc::new(LanguageRegistry::test()),
|
||||
workspace_store,
|
||||
languages: Arc::new(language_registry),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
|
||||
background_actions: || &[],
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
@@ -226,7 +231,7 @@ impl TestServer {
|
||||
workspace::init(app_state.clone(), cx);
|
||||
audio::init((), cx);
|
||||
call::init(client.clone(), user_store.clone(), cx);
|
||||
channel::init(&client);
|
||||
channel::init(&client, user_store, cx);
|
||||
});
|
||||
|
||||
client
|
||||
@@ -237,6 +242,7 @@ impl TestServer {
|
||||
let client = TestClient {
|
||||
app_state,
|
||||
username: name.to_string(),
|
||||
channel_store: cx.read(ChannelStore::global).clone(),
|
||||
state: Default::default(),
|
||||
};
|
||||
client.wait_for_current_user(cx).await;
|
||||
@@ -251,6 +257,19 @@ impl TestServer {
|
||||
.store(true, SeqCst);
|
||||
}
|
||||
|
||||
pub fn simulate_long_connection_interruption(
|
||||
&self,
|
||||
peer_id: PeerId,
|
||||
deterministic: &Arc<Deterministic>,
|
||||
) {
|
||||
self.forbid_connections();
|
||||
self.disconnect_client(peer_id);
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
self.allow_connections();
|
||||
deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
deterministic.run_until_parked();
|
||||
}
|
||||
|
||||
pub fn forbid_connections(&self) {
|
||||
self.forbid_connections.store(true, SeqCst);
|
||||
}
|
||||
@@ -292,10 +311,9 @@ impl TestServer {
|
||||
admin: (&TestClient, &mut TestAppContext),
|
||||
members: &mut [(&TestClient, &mut TestAppContext)],
|
||||
) -> u64 {
|
||||
let (admin_client, admin_cx) = admin;
|
||||
let channel_id = admin_client
|
||||
.app_state
|
||||
.channel_store
|
||||
let (_, admin_cx) = admin;
|
||||
let channel_id = admin_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.create_channel(channel, parent, cx)
|
||||
})
|
||||
@@ -303,9 +321,8 @@ impl TestServer {
|
||||
.unwrap();
|
||||
|
||||
for (member_client, member_cx) in members {
|
||||
admin_client
|
||||
.app_state
|
||||
.channel_store
|
||||
admin_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(admin_cx, |channel_store, cx| {
|
||||
channel_store.invite_member(
|
||||
channel_id,
|
||||
@@ -319,9 +336,8 @@ impl TestServer {
|
||||
|
||||
admin_cx.foreground().run_until_parked();
|
||||
|
||||
member_client
|
||||
.app_state
|
||||
.channel_store
|
||||
member_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(*member_cx, |channels, _| {
|
||||
channels.respond_to_channel_invite(channel_id, true)
|
||||
})
|
||||
@@ -429,7 +445,7 @@ impl TestClient {
|
||||
}
|
||||
|
||||
pub fn channel_store(&self) -> &ModelHandle<ChannelStore> {
|
||||
&self.app_state.channel_store
|
||||
&self.channel_store
|
||||
}
|
||||
|
||||
pub fn user_store(&self) -> &ModelHandle<UserStore> {
|
||||
@@ -536,15 +552,7 @@ impl TestClient {
|
||||
root_path: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (ModelHandle<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
self.client().clone(),
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let project = self.build_empty_local_project(cx);
|
||||
let (worktree, _) = project
|
||||
.update(cx, |p, cx| {
|
||||
p.find_or_create_local_worktree(root_path, true, cx)
|
||||
@@ -557,6 +565,19 @@ impl TestClient {
|
||||
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||
}
|
||||
|
||||
pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> ModelHandle<Project> {
|
||||
cx.update(|cx| {
|
||||
Project::local(
|
||||
self.client().clone(),
|
||||
self.app_state.node_runtime.clone(),
|
||||
self.app_state.user_store.clone(),
|
||||
self.app_state.languages.clone(),
|
||||
self.app_state.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn build_remote_project(
|
||||
&self,
|
||||
host_project_id: u64,
|
||||
@@ -592,8 +613,8 @@ impl TestClient {
|
||||
) {
|
||||
let (other_client, other_cx) = user;
|
||||
|
||||
self.app_state
|
||||
.channel_store
|
||||
cx_self
|
||||
.read(ChannelStore::global)
|
||||
.update(cx_self, |channel_store, cx| {
|
||||
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
|
||||
})
|
||||
@@ -602,11 +623,10 @@ impl TestClient {
|
||||
|
||||
cx_self.foreground().run_until_parked();
|
||||
|
||||
other_client
|
||||
.app_state
|
||||
.channel_store
|
||||
.update(other_cx, |channels, _| {
|
||||
channels.respond_to_channel_invite(channel, true)
|
||||
other_cx
|
||||
.read(ChannelStore::global)
|
||||
.update(other_cx, |channel_store, _| {
|
||||
channel_store.respond_to_channel_invite(channel, true)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -37,6 +37,7 @@ fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
menu = { path = "../menu" }
|
||||
rich_text = { path = "../rich_text" }
|
||||
picker = { path = "../picker" }
|
||||
project = { path = "../project" }
|
||||
recent_projects = {path = "../recent_projects"}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use call::ActiveCall;
|
||||
use channel::{ChannelBuffer, ChannelBufferEvent, ChannelId};
|
||||
use client::proto;
|
||||
use clock::ReplicaId;
|
||||
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::Editor;
|
||||
use editor::{CollaborationHub, Editor};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{ChildView, Label},
|
||||
@@ -13,54 +15,57 @@ use gpui::{
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
use project::Project;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemHandle},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId,
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
actions!(channel_view, [Deploy]);
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
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,
|
||||
}
|
||||
|
||||
impl ChannelView {
|
||||
pub fn deploy(channel_id: ChannelId, workspace: ViewHandle<Workspace>, cx: &mut AppContext) {
|
||||
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(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
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| {
|
||||
let room_id = ActiveCall::global(cx)
|
||||
.read(cx)
|
||||
.room()
|
||||
.map(|room| room.read(cx).id());
|
||||
ActiveCall::report_call_event_for_room(
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
room_id,
|
||||
Some(channel_id),
|
||||
channel_id,
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(channel_view), true, true, None, cx);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
});
|
||||
anyhow::Ok(())
|
||||
anyhow::Ok(channel_view)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn open(
|
||||
pub fn open_in_pane(
|
||||
channel_id: ChannelId,
|
||||
pane: ViewHandle<Pane>,
|
||||
workspace: ViewHandle<Workspace>,
|
||||
@@ -68,7 +73,7 @@ impl ChannelView {
|
||||
) -> Task<Result<ViewHandle<Self>>> {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
let channel_store = workspace.app_state().channel_store.clone();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let markdown = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
@@ -79,17 +84,45 @@ impl ChannelView {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
|
||||
let markdown = markdown.await?;
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
})
|
||||
});
|
||||
if let Some(markdown) = markdown.await.log_err() {
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
pane.items_of_type::<Self>()
|
||||
.find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer)
|
||||
.unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, 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);
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
view
|
||||
})
|
||||
.ok_or_else(|| anyhow!("pane was dropped"))
|
||||
})
|
||||
@@ -97,44 +130,35 @@ impl ChannelView {
|
||||
|
||||
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| Editor::for_buffer(buffer, None, cx));
|
||||
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
|
||||
});
|
||||
let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||
|
||||
cx.subscribe(&project, Self::handle_project_event).detach();
|
||||
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
.detach();
|
||||
|
||||
let this = Self {
|
||||
Self {
|
||||
editor,
|
||||
project,
|
||||
channel_store,
|
||||
channel_buffer,
|
||||
remote_id: None,
|
||||
_editor_event_subscription,
|
||||
};
|
||||
this.refresh_replica_id_map(cx);
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::RemoteIdChanged(_) => {}
|
||||
project::Event::DisconnectedFromHost => {}
|
||||
project::Event::Closed => {}
|
||||
project::Event::CollaboratorUpdated { .. } => {}
|
||||
project::Event::CollaboratorLeft(_) => {}
|
||||
project::Event::CollaboratorJoined(_) => {}
|
||||
_ => return,
|
||||
}
|
||||
self.refresh_replica_id_map(cx);
|
||||
pub fn channel(&self, cx: &AppContext) -> Arc<Channel> {
|
||||
self.channel_buffer.read(cx).channel()
|
||||
}
|
||||
|
||||
fn handle_channel_buffer_event(
|
||||
@@ -144,48 +168,41 @@ impl ChannelView {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelBufferEvent::CollaboratorsChanged => {
|
||||
self.refresh_replica_id_map(cx);
|
||||
}
|
||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(true);
|
||||
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,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a mapping of channel buffer replica ids to the corresponding
|
||||
/// replica ids in the current project.
|
||||
///
|
||||
/// Using this mapping, a given user can be displayed with the same color
|
||||
/// in the channel buffer as in other files in the project. Users who are
|
||||
/// in the channel buffer but not the project will not have a color.
|
||||
fn refresh_replica_id_map(&self, cx: &mut ViewContext<Self>) {
|
||||
let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default();
|
||||
let project = self.project.read(cx);
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
project_replica_ids_by_channel_buffer_replica_id
|
||||
.insert(channel_buffer.replica_id(cx), project.replica_id());
|
||||
project_replica_ids_by_channel_buffer_replica_id.extend(
|
||||
channel_buffer
|
||||
.collaborators()
|
||||
.iter()
|
||||
.filter_map(|channel_buffer_collaborator| {
|
||||
project
|
||||
.collaborators()
|
||||
.values()
|
||||
.find_map(|project_collaborator| {
|
||||
(project_collaborator.user_id == channel_buffer_collaborator.user_id)
|
||||
.then_some((
|
||||
channel_buffer_collaborator.replica_id as ReplicaId,
|
||||
project_collaborator.replica_id,
|
||||
))
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -205,6 +222,7 @@ impl View for ChannelView {
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
@@ -244,6 +262,7 @@ impl Item for ChannelView {
|
||||
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,
|
||||
))
|
||||
@@ -287,10 +306,14 @@ impl FollowableItem for ChannelView {
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
let channel = self.channel_buffer.read(cx).channel();
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
if !channel_buffer.is_connected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(proto::view::Variant::ChannelView(
|
||||
proto::view::ChannelView {
|
||||
channel_id: channel.id,
|
||||
channel_id: channel_buffer.channel().id,
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
{
|
||||
@@ -316,7 +339,7 @@ impl FollowableItem for ChannelView {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let open = ChannelView::open(state.channel_id, pane, workspace, cx);
|
||||
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
@@ -376,17 +399,32 @@ impl FollowableItem for ChannelView {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_leader_replica_id(
|
||||
&mut self,
|
||||
leader_replica_id: Option<u16>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
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_replica_id(leader_replica_id, cx)
|
||||
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||
Editor::should_unfollow_on_event(event, cx)
|
||||
}
|
||||
|
||||
fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||
|
||||
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,6 +3,7 @@ use anyhow::Result;
|
||||
use call::ActiveCall;
|
||||
use channel::{ChannelChat, ChannelChatEvent, ChannelMessageId, ChannelStore};
|
||||
use client::Client;
|
||||
use collections::HashMap;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::Editor;
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||
@@ -12,12 +13,13 @@ use gpui::{
|
||||
platform::{CursorStyle, MouseButton},
|
||||
serde_json,
|
||||
views::{ItemType, Select, SelectStyle},
|
||||
AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyViewHandle, AppContext, AsyncAppContext, Entity, ImageData, ModelHandle, Subscription, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{language_settings::SoftWrap, LanguageRegistry};
|
||||
use menu::Confirm;
|
||||
use project::Fs;
|
||||
use rich_text::RichText;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
@@ -35,6 +37,7 @@ const CHAT_PANEL_KEY: &'static str = "ChatPanel";
|
||||
pub struct ChatPanel {
|
||||
client: Arc<Client>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
active_chat: Option<(ModelHandle<ChannelChat>, Subscription)>,
|
||||
message_list: ListState<ChatPanel>,
|
||||
input_editor: ViewHandle<Editor>,
|
||||
@@ -42,10 +45,12 @@ pub struct ChatPanel {
|
||||
local_timezone: UtcOffset,
|
||||
fs: Arc<dyn Fs>,
|
||||
width: Option<f32>,
|
||||
active: bool,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
subscriptions: Vec<gpui::Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
has_focus: bool,
|
||||
markdown_data: HashMap<ChannelMessageId, RichText>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -76,7 +81,8 @@ impl ChatPanel {
|
||||
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let channel_store = workspace.app_state().channel_store.clone();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let languages = workspace.app_state().languages.clone();
|
||||
|
||||
let input_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::auto_height(
|
||||
@@ -129,6 +135,8 @@ impl ChatPanel {
|
||||
fs,
|
||||
client,
|
||||
channel_store,
|
||||
languages,
|
||||
|
||||
active_chat: Default::default(),
|
||||
pending_serialization: Task::ready(None),
|
||||
message_list,
|
||||
@@ -138,7 +146,9 @@ impl ChatPanel {
|
||||
has_focus: false,
|
||||
subscriptions: Vec::new(),
|
||||
workspace: workspace_handle,
|
||||
active: false,
|
||||
width: None,
|
||||
markdown_data: Default::default(),
|
||||
};
|
||||
|
||||
let mut old_dock_position = this.position(cx);
|
||||
@@ -154,9 +164,9 @@ impl ChatPanel {
|
||||
}),
|
||||
);
|
||||
|
||||
this.init_active_channel(cx);
|
||||
this.update_channel_count(cx);
|
||||
cx.observe(&this.channel_store, |this, _, cx| {
|
||||
this.init_active_channel(cx);
|
||||
this.update_channel_count(cx)
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -175,10 +185,33 @@ impl ChatPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let markdown = this.languages.language_for_name("Markdown");
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.input_editor.update(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |multi_buffer, cx| {
|
||||
multi_buffer
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| buffer.set_language(Some(markdown), cx))
|
||||
})
|
||||
})
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
pub fn active_chat(&self) -> Option<ModelHandle<ChannelChat>> {
|
||||
self.active_chat.as_ref().map(|(chat, _)| chat.clone())
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: AsyncAppContext,
|
||||
@@ -225,10 +258,8 @@ impl ChatPanel {
|
||||
);
|
||||
}
|
||||
|
||||
fn init_active_channel(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn update_channel_count(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let channel_count = self.channel_store.read(cx).channel_count();
|
||||
self.message_list.reset(0);
|
||||
self.active_chat = None;
|
||||
self.channel_select.update(cx, |select, cx| {
|
||||
select.set_item_count(channel_count, cx);
|
||||
});
|
||||
@@ -247,6 +278,7 @@ impl ChatPanel {
|
||||
}
|
||||
let subscription = cx.subscribe(&chat, Self::channel_did_change);
|
||||
self.active_chat = Some((chat, subscription));
|
||||
self.acknowledge_last_message(cx);
|
||||
self.channel_select.update(cx, |select, cx| {
|
||||
if let Some(ix) = self.channel_store.read(cx).index_of_channel(id) {
|
||||
select.set_selected_index(ix, cx);
|
||||
@@ -268,11 +300,34 @@ impl ChatPanel {
|
||||
new_count,
|
||||
} => {
|
||||
self.message_list.splice(old_range.clone(), *new_count);
|
||||
if self.active {
|
||||
self.acknowledge_last_message(cx);
|
||||
}
|
||||
}
|
||||
ChannelChatEvent::NewMessage {
|
||||
channel_id,
|
||||
message_id,
|
||||
} => {
|
||||
if !self.active {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store.new_message(*channel_id, *message_id, cx)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn acknowledge_last_message(&mut self, cx: &mut ViewContext<'_, '_, ChatPanel>) {
|
||||
if self.active {
|
||||
if let Some((chat, _)) = &self.active_chat {
|
||||
chat.update(cx, |chat, cx| {
|
||||
chat.acknowledge_last_message(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_channel(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
@@ -299,13 +354,33 @@ impl ChatPanel {
|
||||
messages.flex(1., true).into_any()
|
||||
}
|
||||
|
||||
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let message = self.active_chat.as_ref().unwrap().0.read(cx).message(ix);
|
||||
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let (message, is_continuation, is_last) = {
|
||||
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
|
||||
let last_message = active_chat.message(ix.saturating_sub(1));
|
||||
let this_message = active_chat.message(ix);
|
||||
let is_continuation = last_message.id != this_message.id
|
||||
&& this_message.sender.id == last_message.sender.id;
|
||||
|
||||
(
|
||||
active_chat.message(ix).clone(),
|
||||
is_continuation,
|
||||
active_chat.message_count() == ix + 1,
|
||||
)
|
||||
};
|
||||
|
||||
let is_pending = message.is_pending();
|
||||
let text = self
|
||||
.markdown_data
|
||||
.entry(message.id)
|
||||
.or_insert_with(|| rich_text::render_markdown(message.body, &self.languages, None));
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let theme = theme::current(cx);
|
||||
let style = if message.is_pending() {
|
||||
let style = if is_pending {
|
||||
&theme.chat_panel.pending_message
|
||||
} else if is_continuation {
|
||||
&theme.chat_panel.continuation_message
|
||||
} else {
|
||||
&theme.chat_panel.message
|
||||
};
|
||||
@@ -318,52 +393,90 @@ impl ChatPanel {
|
||||
None
|
||||
};
|
||||
|
||||
enum DeleteMessage {}
|
||||
|
||||
let body = message.body.clone();
|
||||
Flex::column()
|
||||
.with_child(
|
||||
enum MessageBackgroundHighlight {}
|
||||
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
|
||||
let container = style.container.style_for(state);
|
||||
if is_continuation {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.sender.github_login.clone(),
|
||||
style.sender.text.clone(),
|
||||
text.element(
|
||||
theme.editor.syntax.clone(),
|
||||
style.body.clone(),
|
||||
theme.editor.document_highlight_read_background,
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.sender.container),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(render_remove(message_id_to_remove, cx, &theme))
|
||||
.contained()
|
||||
.with_style(*container)
|
||||
.with_margin_bottom(if is_last {
|
||||
theme.chat_panel.last_message_bottom_spacing
|
||||
} else {
|
||||
0.
|
||||
})
|
||||
.into_any()
|
||||
} else {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(render_avatar(
|
||||
message.sender.avatar.clone(),
|
||||
&theme,
|
||||
))
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.sender.github_login.clone(),
|
||||
style.sender.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.sender.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format_timestamp(
|
||||
message.timestamp,
|
||||
now,
|
||||
self.local_timezone,
|
||||
),
|
||||
style.timestamp.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.timestamp.container),
|
||||
)
|
||||
.align_children_center()
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(render_remove(message_id_to_remove, cx, &theme))
|
||||
.align_children_center(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format_timestamp(message.timestamp, now, self.local_timezone),
|
||||
style.timestamp.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.timestamp.container),
|
||||
Flex::row()
|
||||
.with_child(
|
||||
text.element(
|
||||
theme.editor.syntax.clone(),
|
||||
style.body.clone(),
|
||||
theme.editor.document_highlight_read_background,
|
||||
cx,
|
||||
)
|
||||
.flex(1., true),
|
||||
)
|
||||
// Add a spacer to make everything line up
|
||||
.with_child(render_remove(None, cx, &theme)),
|
||||
)
|
||||
.with_children(message_id_to_remove.map(|id| {
|
||||
MouseEventHandler::new::<DeleteMessage, _>(
|
||||
id as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
let button_style =
|
||||
theme.chat_panel.icon_button.style_for(mouse_state);
|
||||
render_icon_button(button_style, "icons/x.svg")
|
||||
.aligned()
|
||||
.into_any()
|
||||
},
|
||||
)
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_message(id, cx);
|
||||
})
|
||||
.flex_float()
|
||||
})),
|
||||
)
|
||||
.with_child(Text::new(body, style.body.clone()))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.into_any()
|
||||
.contained()
|
||||
.with_style(*container)
|
||||
.with_margin_bottom(if is_last {
|
||||
theme.chat_panel.last_message_bottom_spacing
|
||||
} else {
|
||||
0.
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_input_box(&self, theme: &Arc<Theme>, cx: &AppContext) -> AnyElement<Self> {
|
||||
@@ -409,7 +522,7 @@ impl ChatPanel {
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
if let Some(workspace) = workspace.upgrade(cx) {
|
||||
ChannelView::deploy(channel_id, workspace, cx);
|
||||
ChannelView::open(channel_id, workspace, cx).detach();
|
||||
}
|
||||
})
|
||||
.with_tooltip::<OpenChannelNotes>(
|
||||
@@ -537,6 +650,7 @@ impl ChatPanel {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let chat = open_chat.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.markdown_data = Default::default();
|
||||
this.set_active_chat(chat, cx);
|
||||
})
|
||||
})
|
||||
@@ -546,7 +660,7 @@ impl ChatPanel {
|
||||
if let Some((chat, _)) = &self.active_chat {
|
||||
let channel_id = chat.read(cx).channel().id;
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
ChannelView::deploy(channel_id, workspace, cx);
|
||||
ChannelView::open(channel_id, workspace, cx).detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -561,6 +675,72 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_avatar(avatar: Option<Arc<ImageData>>, theme: &Arc<Theme>) -> AnyElement<ChatPanel> {
|
||||
let avatar_style = theme.chat_panel.avatar;
|
||||
|
||||
avatar
|
||||
.map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(avatar_style.image)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_corner_radius(avatar_style.outer_corner_radius)
|
||||
.constrained()
|
||||
.with_width(avatar_style.outer_width)
|
||||
.with_height(avatar_style.outer_width)
|
||||
.into_any()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(avatar_style.outer_width)
|
||||
.into_any()
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.chat_panel.avatar_container)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_remove(
|
||||
message_id_to_remove: Option<u64>,
|
||||
cx: &mut ViewContext<'_, '_, ChatPanel>,
|
||||
theme: &Arc<Theme>,
|
||||
) -> AnyElement<ChatPanel> {
|
||||
enum DeleteMessage {}
|
||||
|
||||
message_id_to_remove
|
||||
.map(|id| {
|
||||
MouseEventHandler::new::<DeleteMessage, _>(id as usize, cx, |mouse_state, _| {
|
||||
let button_style = theme.chat_panel.icon_button.style_for(mouse_state);
|
||||
render_icon_button(button_style, "icons/x.svg")
|
||||
.aligned()
|
||||
.into_any()
|
||||
})
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_message(id, cx);
|
||||
})
|
||||
.flex_float()
|
||||
.into_any()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
let style = theme.chat_panel.icon_button.default;
|
||||
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_uniform_padding(2.)
|
||||
.flex_float()
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
impl Entity for ChatPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
@@ -627,8 +807,12 @@ impl Panel for ChatPanel {
|
||||
}
|
||||
|
||||
fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if active && !is_chat_feature_enabled(cx) {
|
||||
cx.emit(Event::Dismissed);
|
||||
self.active = active;
|
||||
if active {
|
||||
self.acknowledge_last_message(cx);
|
||||
if !is_chat_feature_enabled(cx) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ use crate::{
|
||||
contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
|
||||
toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
|
||||
};
|
||||
use auto_update::AutoUpdateStatus;
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
@@ -215,7 +216,13 @@ impl CollabTitlebarItem {
|
||||
let git_style = theme.titlebar.git_menu_button.clone();
|
||||
let item_spacing = theme.titlebar.item_spacing;
|
||||
|
||||
let mut ret = Flex::row().with_child(
|
||||
let mut ret = Flex::row();
|
||||
|
||||
if let Some(project_host) = self.collect_project_host(theme.clone(), cx) {
|
||||
ret = ret.with_child(project_host)
|
||||
}
|
||||
|
||||
ret = ret.with_child(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
|
||||
@@ -283,6 +290,71 @@ impl CollabTitlebarItem {
|
||||
ret.into_any()
|
||||
}
|
||||
|
||||
fn collect_project_host(
|
||||
&self,
|
||||
theme: Arc<Theme>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
if ActiveCall::global(cx).read(cx).room().is_none() {
|
||||
return None;
|
||||
}
|
||||
let project = self.project.read(cx);
|
||||
let user_store = self.user_store.read(cx);
|
||||
|
||||
if project.is_local() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(host) = project.host() else {
|
||||
return None;
|
||||
};
|
||||
let (Some(host_user), Some(participant_index)) = (
|
||||
user_store.get_cached_user(host.user_id),
|
||||
user_store.participant_indices().get(&host.user_id),
|
||||
) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
enum ProjectHost {}
|
||||
enum ProjectHostTooltip {}
|
||||
|
||||
let host_style = theme.titlebar.project_host.clone();
|
||||
let selection_style = theme
|
||||
.editor
|
||||
.selection_style_for_room_participant(participant_index.0);
|
||||
let peer_id = host.peer_id.clone();
|
||||
|
||||
Some(
|
||||
MouseEventHandler::new::<ProjectHost, _>(0, cx, |mouse_state, _| {
|
||||
let mut host_style = host_style.style_for(mouse_state).clone();
|
||||
host_style.text.color = selection_style.cursor;
|
||||
Label::new(host_user.github_login.clone(), host_style.text)
|
||||
.contained()
|
||||
.with_style(host_style.container)
|
||||
.aligned()
|
||||
.left()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
if let Some(task) =
|
||||
workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_tooltip::<ProjectHostTooltip>(
|
||||
0,
|
||||
host_user.github_login.clone() + " is sharing this project. Click to follow.",
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any_named("project-host"),
|
||||
)
|
||||
}
|
||||
|
||||
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
let project = if active {
|
||||
Some(self.project.clone())
|
||||
@@ -877,7 +949,7 @@ impl CollabTitlebarItem {
|
||||
fn render_face_pile(
|
||||
&self,
|
||||
user: &User,
|
||||
replica_id: Option<ReplicaId>,
|
||||
_replica_id: Option<ReplicaId>,
|
||||
peer_id: PeerId,
|
||||
location: Option<ParticipantLocation>,
|
||||
muted: bool,
|
||||
@@ -886,23 +958,20 @@ impl CollabTitlebarItem {
|
||||
theme: &Theme,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let user_id = user.id;
|
||||
let project_id = workspace.read(cx).project().read(cx).remote_id();
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
|
||||
let followed_by_self = room
|
||||
.and_then(|room| {
|
||||
Some(
|
||||
is_being_followed
|
||||
&& room
|
||||
.read(cx)
|
||||
.followers_for(peer_id, project_id?)
|
||||
.iter()
|
||||
.any(|&follower| {
|
||||
Some(follower) == workspace.read(cx).client().peer_id()
|
||||
}),
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
let room = ActiveCall::global(cx).read(cx).room().cloned();
|
||||
let self_peer_id = workspace.read(cx).client().peer_id();
|
||||
let self_following = workspace.read(cx).is_being_followed(peer_id);
|
||||
let self_following_initialized = self_following
|
||||
&& room.as_ref().map_or(false, |room| match project_id {
|
||||
None => true,
|
||||
Some(project_id) => room
|
||||
.read(cx)
|
||||
.followers_for(peer_id, project_id)
|
||||
.iter()
|
||||
.any(|&follower| Some(follower) == self_peer_id),
|
||||
});
|
||||
|
||||
let leader_style = theme.titlebar.leader_avatar;
|
||||
let follower_style = theme.titlebar.follower_avatar;
|
||||
@@ -921,147 +990,131 @@ impl CollabTitlebarItem {
|
||||
.background_color
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(replica_id) = replica_id {
|
||||
if followed_by_self {
|
||||
let selection = theme.editor.replica_selection_style(replica_id).selection;
|
||||
let participant_index = self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.participant_indices()
|
||||
.get(&user_id)
|
||||
.copied();
|
||||
if let Some(participant_index) = participant_index {
|
||||
if self_following_initialized {
|
||||
let selection = theme
|
||||
.editor
|
||||
.selection_style_for_room_participant(participant_index.0)
|
||||
.selection;
|
||||
background_color = Color::blend(selection, background_color);
|
||||
background_color.a = 255;
|
||||
}
|
||||
}
|
||||
|
||||
let mut content = Stack::new()
|
||||
.with_children(user.avatar.as_ref().map(|avatar| {
|
||||
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
|
||||
.with_child(Self::render_face(
|
||||
avatar.clone(),
|
||||
Self::location_style(workspace, location, leader_style, cx),
|
||||
background_color,
|
||||
microphone_state,
|
||||
))
|
||||
.with_children(
|
||||
(|| {
|
||||
let project_id = project_id?;
|
||||
let room = room?.read(cx);
|
||||
let followers = room.followers_for(peer_id, project_id);
|
||||
enum TitlebarParticipant {}
|
||||
|
||||
Some(followers.into_iter().flat_map(|&follower| {
|
||||
let remote_participant =
|
||||
room.remote_participant_for_peer_id(follower);
|
||||
|
||||
let avatar = remote_participant
|
||||
.and_then(|p| p.user.avatar.clone())
|
||||
.or_else(|| {
|
||||
if follower == workspace.read(cx).client().peer_id()? {
|
||||
workspace
|
||||
.read(cx)
|
||||
.user_store()
|
||||
.read(cx)
|
||||
.current_user()?
|
||||
.avatar
|
||||
.clone()
|
||||
} else {
|
||||
None
|
||||
let content = MouseEventHandler::new::<TitlebarParticipant, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
cx,
|
||||
move |_, cx| {
|
||||
Stack::new()
|
||||
.with_children(user.avatar.as_ref().map(|avatar| {
|
||||
let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap)
|
||||
.with_child(Self::render_face(
|
||||
avatar.clone(),
|
||||
Self::location_style(workspace, location, leader_style, cx),
|
||||
background_color,
|
||||
microphone_state,
|
||||
))
|
||||
.with_children(
|
||||
(|| {
|
||||
let project_id = project_id?;
|
||||
let room = room?.read(cx);
|
||||
let followers = room.followers_for(peer_id, project_id);
|
||||
Some(followers.into_iter().filter_map(|&follower| {
|
||||
if Some(follower) == self_peer_id {
|
||||
return None;
|
||||
}
|
||||
})?;
|
||||
let participant =
|
||||
room.remote_participant_for_peer_id(follower)?;
|
||||
Some(Self::render_face(
|
||||
participant.user.avatar.clone()?,
|
||||
follower_style,
|
||||
background_color,
|
||||
None,
|
||||
))
|
||||
}))
|
||||
})()
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.with_children(
|
||||
self_following_initialized
|
||||
.then(|| self.user_store.read(cx).current_user())
|
||||
.and_then(|user| {
|
||||
Some(Self::render_face(
|
||||
user?.avatar.clone()?,
|
||||
follower_style,
|
||||
background_color,
|
||||
None,
|
||||
))
|
||||
}),
|
||||
);
|
||||
|
||||
Some(Self::render_face(
|
||||
avatar.clone(),
|
||||
follower_style,
|
||||
background_color,
|
||||
None,
|
||||
))
|
||||
}))
|
||||
})()
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
);
|
||||
let mut container = face_pile
|
||||
.contained()
|
||||
.with_style(theme.titlebar.leader_selection);
|
||||
|
||||
let mut container = face_pile
|
||||
.contained()
|
||||
.with_style(theme.titlebar.leader_selection);
|
||||
|
||||
if let Some(replica_id) = replica_id {
|
||||
if followed_by_self {
|
||||
let color = theme.editor.replica_selection_style(replica_id).selection;
|
||||
container = container.with_background_color(color);
|
||||
}
|
||||
}
|
||||
|
||||
container
|
||||
}))
|
||||
.with_children((|| {
|
||||
let replica_id = replica_id?;
|
||||
let color = theme.editor.replica_selection_style(replica_id).cursor;
|
||||
Some(
|
||||
AvatarRibbon::new(color)
|
||||
.constrained()
|
||||
.with_width(theme.titlebar.avatar_ribbon.width)
|
||||
.with_height(theme.titlebar.avatar_ribbon.height)
|
||||
.aligned()
|
||||
.bottom(),
|
||||
)
|
||||
})())
|
||||
.into_any();
|
||||
|
||||
if let Some(location) = location {
|
||||
if let Some(replica_id) = replica_id {
|
||||
enum ToggleFollow {}
|
||||
|
||||
content = MouseEventHandler::new::<ToggleFollow, _>(
|
||||
replica_id.into(),
|
||||
cx,
|
||||
move |_, _| content,
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, item, cx| {
|
||||
if let Some(workspace) = item.workspace.upgrade(cx) {
|
||||
if let Some(task) = workspace
|
||||
.update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
if let Some(participant_index) = participant_index {
|
||||
if self_following_initialized {
|
||||
let color = theme
|
||||
.editor
|
||||
.selection_style_for_room_participant(participant_index.0)
|
||||
.selection;
|
||||
container = container.with_background_color(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_tooltip::<ToggleFollow>(
|
||||
peer_id.as_u64() as usize,
|
||||
if is_being_followed {
|
||||
format!("Unfollow {}", user.github_login)
|
||||
} else {
|
||||
format!("Follow {}", user.github_login)
|
||||
},
|
||||
Some(Box::new(FollowNextCollaborator)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any();
|
||||
} else if let ParticipantLocation::SharedProject { project_id } = location {
|
||||
enum JoinProject {}
|
||||
|
||||
let user_id = user.id;
|
||||
content = MouseEventHandler::new::<JoinProject, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
cx,
|
||||
move |_, _| content,
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
let app_state = workspace.read(cx).app_state().clone();
|
||||
workspace::join_remote_project(project_id, user_id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<JoinProject>(
|
||||
peer_id.as_u64() as usize,
|
||||
format!("Follow {} into external project", user.github_login),
|
||||
Some(Box::new(FollowNextCollaborator)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
container
|
||||
}))
|
||||
.with_children((|| {
|
||||
let participant_index = participant_index?;
|
||||
let color = theme
|
||||
.editor
|
||||
.selection_style_for_room_participant(participant_index.0)
|
||||
.cursor;
|
||||
Some(
|
||||
AvatarRibbon::new(color)
|
||||
.constrained()
|
||||
.with_width(theme.titlebar.avatar_ribbon.width)
|
||||
.with_height(theme.titlebar.avatar_ribbon.height)
|
||||
.aligned()
|
||||
.bottom(),
|
||||
)
|
||||
})())
|
||||
},
|
||||
);
|
||||
|
||||
if Some(peer_id) == self_peer_id {
|
||||
return content.into_any();
|
||||
}
|
||||
|
||||
content
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
let Some(workspace) = this.workspace.upgrade(cx) else {
|
||||
return;
|
||||
};
|
||||
if let Some(task) =
|
||||
workspace.update(cx, |workspace, cx| workspace.follow(peer_id, cx))
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<TitlebarParticipant>(
|
||||
peer_id.as_u64() as usize,
|
||||
format!("Follow {}", user.github_login),
|
||||
Some(Box::new(FollowNextCollaborator)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn location_style(
|
||||
@@ -1125,22 +1178,38 @@ impl CollabTitlebarItem {
|
||||
.with_style(theme.titlebar.offline_icon.container)
|
||||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => Some(
|
||||
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
Label::new(
|
||||
"Please update Zed to collaborate",
|
||||
theme.titlebar.outdated_warning.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.titlebar.outdated_warning.container)
|
||||
.aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
auto_update::check(&Default::default(), cx);
|
||||
})
|
||||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => {
|
||||
let auto_updater = auto_update::AutoUpdater::get(cx);
|
||||
let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
|
||||
Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
|
||||
Some(AutoUpdateStatus::Installing)
|
||||
| Some(AutoUpdateStatus::Downloading)
|
||||
| Some(AutoUpdateStatus::Checking) => "Updating...",
|
||||
Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
|
||||
"Please update Zed to Collaborate"
|
||||
}
|
||||
};
|
||||
|
||||
Some(
|
||||
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
Label::new(label, theme.titlebar.outdated_warning.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.titlebar.outdated_warning.container)
|
||||
.aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
|
||||
if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
|
||||
workspace::restart(&Default::default(), cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
auto_update::check(&Default::default(), cx);
|
||||
})
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ mod face_pile;
|
||||
mod incoming_call_notification;
|
||||
mod notifications;
|
||||
mod panel_settings;
|
||||
mod project_shared_notification;
|
||||
pub mod project_shared_notification;
|
||||
mod sharing_status_indicator;
|
||||
|
||||
use call::{ActiveCall, Room};
|
||||
use call::{report_call_event_for_room, ActiveCall, Room};
|
||||
use gpui::{
|
||||
actions,
|
||||
geometry::{
|
||||
@@ -55,18 +55,18 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
let client = call.client();
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
ActiveCall::report_call_event_for_room(
|
||||
report_call_event_for_room(
|
||||
"disable screen share",
|
||||
Some(room.id()),
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
ActiveCall::report_call_event_for_room(
|
||||
report_call_event_for_room(
|
||||
"enable screen share",
|
||||
Some(room.id()),
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
@@ -83,23 +83,13 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
room.update(cx, |room, cx| {
|
||||
if room.is_muted(cx) {
|
||||
ActiveCall::report_call_event_for_room(
|
||||
"enable microphone",
|
||||
Some(room.id()),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
let operation = if room.is_muted(cx) {
|
||||
"enable microphone"
|
||||
} else {
|
||||
ActiveCall::report_call_event_for_room(
|
||||
"disable microphone",
|
||||
Some(room.id()),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
"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))
|
||||
|
||||
@@ -40,7 +40,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
.push(window);
|
||||
}
|
||||
}
|
||||
room::Event::RemoteProjectUnshared { project_id } => {
|
||||
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);
|
||||
@@ -82,7 +84,6 @@ impl ProjectSharedNotification {
|
||||
}
|
||||
|
||||
fn join(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.remove_window();
|
||||
if let Some(app_state) = self.app_state.upgrade() {
|
||||
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -90,7 +91,15 @@ impl ProjectSharedNotification {
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.remove_window();
|
||||
if let Some(active_room) =
|
||||
ActiveCall::global(cx).read_with(cx, |call, _| call.room().cloned())
|
||||
{
|
||||
active_room.update(cx, |_, cx| {
|
||||
cx.emit(room::Event::RemoteProjectInvitationDiscarded {
|
||||
project_id: self.project_id,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
|
||||
@@ -18,6 +18,15 @@ actions!(command_palette, [Toggle]);
|
||||
|
||||
pub type CommandPalette = Picker<CommandPaletteDelegate>;
|
||||
|
||||
pub type CommandPaletteInterceptor =
|
||||
Box<dyn Fn(&str, &AppContext) -> Option<CommandInterceptResult>>;
|
||||
|
||||
pub struct CommandInterceptResult {
|
||||
pub action: Box<dyn Action>,
|
||||
pub string: String,
|
||||
pub positions: Vec<usize>,
|
||||
}
|
||||
|
||||
pub struct CommandPaletteDelegate {
|
||||
actions: Vec<Command>,
|
||||
matches: Vec<StringMatch>,
|
||||
@@ -117,7 +126,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let actions = cx.read(move |cx| {
|
||||
let mut actions = cx.read(move |cx| {
|
||||
let hit_counts = cx.optional_global::<HitCounts>();
|
||||
actions.sort_by_key(|action| {
|
||||
(
|
||||
@@ -136,7 +145,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if query.is_empty() {
|
||||
let mut matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -158,6 +167,40 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
)
|
||||
.await
|
||||
};
|
||||
let intercept_result = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteInterceptor>() {
|
||||
cx.global::<CommandPaletteInterceptor>()(&query, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
positions,
|
||||
}) = intercept_result
|
||||
{
|
||||
if let Some(idx) = matches
|
||||
.iter()
|
||||
.position(|m| actions[m.candidate_id].action.id() == action.id())
|
||||
{
|
||||
matches.remove(idx);
|
||||
}
|
||||
actions.push(Command {
|
||||
name: string.clone(),
|
||||
action,
|
||||
keystrokes: vec![],
|
||||
});
|
||||
matches.insert(
|
||||
0,
|
||||
StringMatch {
|
||||
candidate_id: actions.len() - 1,
|
||||
string,
|
||||
positions,
|
||||
score: 0.0,
|
||||
},
|
||||
)
|
||||
}
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = picker.delegate_mut();
|
||||
@@ -222,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
.with_children(
|
||||
[
|
||||
(keystroke.ctrl, "^"),
|
||||
(keystroke.alt, "⎇"),
|
||||
(keystroke.alt, "⌥"),
|
||||
(keystroke.cmd, "⌘"),
|
||||
(keystroke.shift, "⇧"),
|
||||
]
|
||||
|
||||
49
crates/copilot2/Cargo.toml
Normal file
49
crates/copilot2/Cargo.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
[package]
|
||||
name = "copilot2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/copilot2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"collections/test-support",
|
||||
"gpui2/test-support",
|
||||
"language2/test-support",
|
||||
"lsp2/test-support",
|
||||
"settings2/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
gpui2 = { path = "../gpui2" }
|
||||
language2 = { path = "../language2" }
|
||||
settings2 = { path = "../settings2" }
|
||||
theme = { path = "../theme" }
|
||||
lsp2 = { path = "../lsp2" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
gpui2 = { path = "../gpui2", features = ["test-support"] }
|
||||
language2 = { path = "../language2", features = ["test-support"] }
|
||||
lsp2 = { path = "../lsp2", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings2 = { path = "../settings2", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
1226
crates/copilot2/src/copilot2.rs
Normal file
1226
crates/copilot2/src/copilot2.rs
Normal file
File diff suppressed because it is too large
Load Diff
225
crates/copilot2/src/request.rs
Normal file
225
crates/copilot2/src/request.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub enum CheckStatus {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CheckStatusParams {
|
||||
pub local_checks_only: bool,
|
||||
}
|
||||
|
||||
impl lsp2::request::Request for CheckStatus {
|
||||
type Params = CheckStatusParams;
|
||||
type Result = SignInStatus;
|
||||
const METHOD: &'static str = "checkStatus";
|
||||
}
|
||||
|
||||
pub enum SignInInitiate {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SignInInitiateParams {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum SignInInitiateResult {
|
||||
AlreadySignedIn { user: String },
|
||||
PromptUserDeviceFlow(PromptUserDeviceFlow),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PromptUserDeviceFlow {
|
||||
pub user_code: String,
|
||||
pub verification_uri: String,
|
||||
}
|
||||
|
||||
impl lsp2::request::Request for SignInInitiate {
|
||||
type Params = SignInInitiateParams;
|
||||
type Result = SignInInitiateResult;
|
||||
const METHOD: &'static str = "signInInitiate";
|
||||
}
|
||||
|
||||
pub enum SignInConfirm {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignInConfirmParams {
|
||||
pub user_code: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "status")]
|
||||
pub enum SignInStatus {
|
||||
#[serde(rename = "OK")]
|
||||
Ok {
|
||||
user: String,
|
||||
},
|
||||
MaybeOk {
|
||||
user: String,
|
||||
},
|
||||
AlreadySignedIn {
|
||||
user: String,
|
||||
},
|
||||
NotAuthorized {
|
||||
user: String,
|
||||
},
|
||||
NotSignedIn,
|
||||
}
|
||||
|
||||
impl lsp2::request::Request for SignInConfirm {
|
||||
type Params = SignInConfirmParams;
|
||||
type Result = SignInStatus;
|
||||
const METHOD: &'static str = "signInConfirm";
|
||||
}
|
||||
|
||||
pub enum SignOut {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignOutParams {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SignOutResult {}
|
||||
|
||||
impl lsp2::request::Request for SignOut {
|
||||
type Params = SignOutParams;
|
||||
type Result = SignOutResult;
|
||||
const METHOD: &'static str = "signOut";
|
||||
}
|
||||
|
||||
pub enum GetCompletions {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsParams {
|
||||
pub doc: GetCompletionsDocument,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsDocument {
|
||||
pub tab_size: u32,
|
||||
pub indent_size: u32,
|
||||
pub insert_spaces: bool,
|
||||
pub uri: lsp2::Url,
|
||||
pub relative_path: String,
|
||||
pub position: lsp2::Position,
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsResult {
|
||||
pub completions: Vec<Completion>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Completion {
|
||||
pub text: String,
|
||||
pub position: lsp2::Position,
|
||||
pub uuid: String,
|
||||
pub range: lsp2::Range,
|
||||
pub display_text: String,
|
||||
}
|
||||
|
||||
impl lsp2::request::Request for GetCompletions {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletions";
|
||||
}
|
||||
|
||||
pub enum GetCompletionsCycling {}
|
||||
|
||||
impl lsp2::request::Request for GetCompletionsCycling {
|
||||
type Params = GetCompletionsParams;
|
||||
type Result = GetCompletionsResult;
|
||||
const METHOD: &'static str = "getCompletionsCycling";
|
||||
}
|
||||
|
||||
pub enum LogMessage {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogMessageParams {
|
||||
pub level: u8,
|
||||
pub message: String,
|
||||
pub metadata_str: String,
|
||||
pub extra: Vec<String>,
|
||||
}
|
||||
|
||||
impl lsp2::notification::Notification for LogMessage {
|
||||
type Params = LogMessageParams;
|
||||
const METHOD: &'static str = "LogMessage";
|
||||
}
|
||||
|
||||
pub enum StatusNotification {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StatusNotificationParams {
|
||||
pub message: String,
|
||||
pub status: String, // One of Normal/InProgress
|
||||
}
|
||||
|
||||
impl lsp2::notification::Notification for StatusNotification {
|
||||
type Params = StatusNotificationParams;
|
||||
const METHOD: &'static str = "statusNotification";
|
||||
}
|
||||
|
||||
pub enum SetEditorInfo {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetEditorInfoParams {
|
||||
pub editor_info: EditorInfo,
|
||||
pub editor_plugin_info: EditorPluginInfo,
|
||||
}
|
||||
|
||||
impl lsp2::request::Request for SetEditorInfo {
|
||||
type Params = SetEditorInfoParams;
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "setEditorInfo";
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorPluginInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub enum NotifyAccepted {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotifyAcceptedParams {
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
impl lsp2::request::Request for NotifyAccepted {
|
||||
type Params = NotifyAcceptedParams;
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "notifyAccepted";
|
||||
}
|
||||
|
||||
pub enum NotifyRejected {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotifyRejectedParams {
|
||||
pub uuids: Vec<String>,
|
||||
}
|
||||
|
||||
impl lsp2::request::Request for NotifyRejected {
|
||||
type Params = NotifyRejectedParams;
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "notifyRejected";
|
||||
}
|
||||
376
crates/copilot2/src/sign_in.rs
Normal file
376
crates/copilot2/src/sign_in.rs
Normal file
@@ -0,0 +1,376 @@
|
||||
// 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;
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct CopyUserCode;
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct OpenGithub;
|
||||
|
||||
// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
|
||||
// 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();
|
||||
|
||||
// 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();
|
||||
// }
|
||||
// }
|
||||
|
||||
// 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())
|
||||
// })
|
||||
// }
|
||||
|
||||
// pub struct CopilotCodeVerification {
|
||||
// status: Status,
|
||||
// connect_clicked: bool,
|
||||
// }
|
||||
|
||||
// impl CopilotCodeVerification {
|
||||
// pub fn new(status: Status) -> Self {
|
||||
// Self {
|
||||
// status,
|
||||
// connect_clicked: false,
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
// self.status = status;
|
||||
// cx.notify();
|
||||
// }
|
||||
|
||||
// fn render_device_code(
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> impl Element<Self> {
|
||||
// let copied = cx
|
||||
// .read_from_clipboard()
|
||||
// .map(|item| item.text() == &data.user_code)
|
||||
// .unwrap_or(false);
|
||||
|
||||
// let device_code_style = &style.auth.prompting.device_code;
|
||||
|
||||
// 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()
|
||||
// }
|
||||
// }
|
||||
33
crates/db2/Cargo.toml
Normal file
33
crates/db2/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "db2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/db2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
gpui2 = { path = "../gpui2" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
sqlez_macros = { path = "../sqlez_macros" }
|
||||
util = { path = "../util" }
|
||||
anyhow.workspace = true
|
||||
indoc.workspace = true
|
||||
async-trait.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui2 = { path = "../gpui2", features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
tempdir.workspace = true
|
||||
5
crates/db2/README.md
Normal file
5
crates/db2/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Building Queries
|
||||
|
||||
First, craft your test data. The examples folder shows a template for building a test-db, and can be ran with `cargo run --example [your-example]`.
|
||||
|
||||
To actually use and test your queries, import the generated DB file into https://sqliteonline.com/
|
||||
327
crates/db2/src/db2.rs
Normal file
327
crates/db2/src/db2.rs
Normal file
@@ -0,0 +1,327 @@
|
||||
pub mod kvp;
|
||||
pub mod query;
|
||||
|
||||
// Re-export
|
||||
pub use anyhow;
|
||||
use anyhow::Context;
|
||||
use gpui2::AppContext;
|
||||
pub use indoc::indoc;
|
||||
pub use lazy_static;
|
||||
pub use smol;
|
||||
pub use sqlez;
|
||||
pub use sqlez_macros;
|
||||
pub use util::channel::{RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
|
||||
pub use util::paths::DB_DIR;
|
||||
|
||||
use sqlez::domain::Migrator;
|
||||
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
use sqlez_macros::sql;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use util::channel::ReleaseChannel;
|
||||
use util::{async_iife, ResultExt};
|
||||
|
||||
const CONNECTION_INITIALIZE_QUERY: &'static str = sql!(
|
||||
PRAGMA foreign_keys=TRUE;
|
||||
);
|
||||
|
||||
const DB_INITIALIZE_QUERY: &'static str = sql!(
|
||||
PRAGMA journal_mode=WAL;
|
||||
PRAGMA busy_timeout=1;
|
||||
PRAGMA case_sensitive_like=TRUE;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
);
|
||||
|
||||
const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
|
||||
|
||||
const DB_FILE_NAME: &'static str = "db.sqlite";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
|
||||
/// Open or create a database at the given directory path.
|
||||
/// This will retry a couple times if there are failures. If opening fails once, the db directory
|
||||
/// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
|
||||
/// In either case, static variables are set so that the user can be notified.
|
||||
pub async fn open_db<M: Migrator + 'static>(
|
||||
db_dir: &Path,
|
||||
release_channel: &ReleaseChannel,
|
||||
) -> ThreadSafeConnection<M> {
|
||||
if *ZED_STATELESS {
|
||||
return open_fallback_db().await;
|
||||
}
|
||||
|
||||
let release_channel_name = release_channel.dev_name();
|
||||
let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
|
||||
|
||||
let connection = async_iife!({
|
||||
smol::fs::create_dir_all(&main_db_dir)
|
||||
.await
|
||||
.context("Could not create db directory")
|
||||
.log_err()?;
|
||||
let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
|
||||
open_main_db(&db_path).await
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(connection) = connection {
|
||||
return connection;
|
||||
}
|
||||
|
||||
// Set another static ref so that we can escalate the notification
|
||||
ALL_FILE_DB_FAILED.store(true, Ordering::Release);
|
||||
|
||||
// If still failed, create an in memory db with a known name
|
||||
open_fallback_db().await
|
||||
}
|
||||
|
||||
async fn open_main_db<M: Migrator>(db_path: &PathBuf) -> Option<ThreadSafeConnection<M>> {
|
||||
log::info!("Opening main db");
|
||||
ThreadSafeConnection::<M>::builder(db_path.to_string_lossy().as_ref(), true)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
.build()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection<M> {
|
||||
log::info!("Opening fallback db");
|
||||
ThreadSafeConnection::<M>::builder(FALLBACK_DB_NAME, false)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
.build()
|
||||
.await
|
||||
.expect(
|
||||
"Fallback in memory database failed. Likely initialization queries or migrations have fundamental errors",
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn open_test_db<M: Migrator>(db_name: &str) -> ThreadSafeConnection<M> {
|
||||
use sqlez::thread_safe_connection::locking_queue;
|
||||
|
||||
ThreadSafeConnection::<M>::builder(db_name, false)
|
||||
.with_db_initialization_query(DB_INITIALIZE_QUERY)
|
||||
.with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
|
||||
// Serialize queued writes via a mutex and run them synchronously
|
||||
.with_write_queue_constructor(locking_queue())
|
||||
.build()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Implements a basic DB wrapper for a given domain
|
||||
#[macro_export]
|
||||
macro_rules! define_connection {
|
||||
(pub static ref $id:ident: $t:ident<()> = $migrations:expr;) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<$t>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
|
||||
}
|
||||
};
|
||||
(pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr;) => {
|
||||
pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection<( $($d),+, $t )>);
|
||||
|
||||
impl ::std::ops::Deref for $t {
|
||||
type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection<($($d),+, $t)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl $crate::sqlez::domain::Domain for $t {
|
||||
fn name() -> &'static str {
|
||||
stringify!($t)
|
||||
}
|
||||
|
||||
fn migrations() -> &'static [&'static str] {
|
||||
$migrations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_test_db(stringify!($id))));
|
||||
}
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
$crate::lazy_static::lazy_static! {
|
||||
pub static ref $id: $t = $t($crate::smol::block_on($crate::open_db(&$crate::DB_DIR, &$crate::RELEASE_CHANNEL)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &mut AppContext, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
where
|
||||
F: Future<Output = anyhow::Result<()>> + Send,
|
||||
{
|
||||
cx.executor()
|
||||
.spawn(async move { db_write().await.log_err() })
|
||||
.detach()
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use std::thread;
|
||||
|
||||
// use sqlez::domain::Domain;
|
||||
// use sqlez_macros::sql;
|
||||
// use tempdir::TempDir;
|
||||
|
||||
// use crate::open_db;
|
||||
|
||||
// // Test bad migration panics
|
||||
// #[gpui::test]
|
||||
// #[should_panic]
|
||||
// async fn test_bad_migration_panics() {
|
||||
// enum BadDB {}
|
||||
|
||||
// impl Domain for BadDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests"
|
||||
// }
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[
|
||||
// sql!(CREATE TABLE test(value);),
|
||||
// // failure because test already exists
|
||||
// sql!(CREATE TABLE test(value);),
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
|
||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
||||
// let _bad_db = open_db::<BadDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
// }
|
||||
|
||||
// /// Test that DB exists but corrupted (causing recreate)
|
||||
// #[gpui::test]
|
||||
// async fn test_db_corruption() {
|
||||
// enum CorruptedDB {}
|
||||
|
||||
// impl Domain for CorruptedDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests"
|
||||
// }
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[sql!(CREATE TABLE test(value);)]
|
||||
// }
|
||||
// }
|
||||
|
||||
// enum GoodDB {}
|
||||
|
||||
// impl Domain for GoodDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests" //Notice same name
|
||||
// }
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
// }
|
||||
// }
|
||||
|
||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
||||
// {
|
||||
// let corrupt_db =
|
||||
// open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
// assert!(corrupt_db.persistent());
|
||||
// }
|
||||
|
||||
// let good_db = open_db::<GoodDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
// assert!(
|
||||
// good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
|
||||
// .unwrap()
|
||||
// .is_none()
|
||||
// );
|
||||
// }
|
||||
|
||||
// /// Test that DB exists but corrupted (causing recreate)
|
||||
// #[gpui::test(iterations = 30)]
|
||||
// async fn test_simultaneous_db_corruption() {
|
||||
// enum CorruptedDB {}
|
||||
|
||||
// impl Domain for CorruptedDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests"
|
||||
// }
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[sql!(CREATE TABLE test(value);)]
|
||||
// }
|
||||
// }
|
||||
|
||||
// enum GoodDB {}
|
||||
|
||||
// impl Domain for GoodDB {
|
||||
// fn name() -> &'static str {
|
||||
// "db_tests" //Notice same name
|
||||
// }
|
||||
|
||||
// fn migrations() -> &'static [&'static str] {
|
||||
// &[sql!(CREATE TABLE test2(value);)] //But different migration
|
||||
// }
|
||||
// }
|
||||
|
||||
// let tempdir = TempDir::new("DbTests").unwrap();
|
||||
// {
|
||||
// // Setup the bad database
|
||||
// let corrupt_db =
|
||||
// open_db::<CorruptedDB>(tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
|
||||
// assert!(corrupt_db.persistent());
|
||||
// }
|
||||
|
||||
// // Try to connect to it a bunch of times at once
|
||||
// let mut guards = vec![];
|
||||
// for _ in 0..10 {
|
||||
// let tmp_path = tempdir.path().to_path_buf();
|
||||
// let guard = thread::spawn(move || {
|
||||
// let good_db = smol::block_on(open_db::<GoodDB>(
|
||||
// tmp_path.as_path(),
|
||||
// &util::channel::ReleaseChannel::Dev,
|
||||
// ));
|
||||
// assert!(
|
||||
// good_db.select_row::<usize>("SELECT * FROM test2").unwrap()()
|
||||
// .unwrap()
|
||||
// .is_none()
|
||||
// );
|
||||
// });
|
||||
|
||||
// guards.push(guard);
|
||||
// }
|
||||
|
||||
// for guard in guards.into_iter() {
|
||||
// assert!(guard.join().is_ok());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
62
crates/db2/src/kvp.rs
Normal file
62
crates/db2/src/kvp.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use sqlez_macros::sql;
|
||||
|
||||
use crate::{define_connection, query};
|
||||
|
||||
define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> =
|
||||
&[sql!(
|
||||
CREATE TABLE IF NOT EXISTS kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
)];
|
||||
);
|
||||
|
||||
impl KeyValueStore {
|
||||
query! {
|
||||
pub fn read_kvp(key: &str) -> Result<Option<String>> {
|
||||
SELECT value FROM kv_store WHERE key = (?)
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn write_kvp(key: String, value: String) -> Result<()> {
|
||||
INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
pub async fn delete_kvp(key: String) -> Result<()> {
|
||||
DELETE FROM kv_store WHERE key = (?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use crate::kvp::KeyValueStore;
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_kvp() {
|
||||
// let db = KeyValueStore(crate::open_test_db("test_kvp").await);
|
||||
|
||||
// assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
|
||||
// db.write_kvp("key-1".to_string(), "one".to_string())
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one".to_string()));
|
||||
|
||||
// db.write_kvp("key-1".to_string(), "one-2".to_string())
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(db.read_kvp("key-1").unwrap(), Some("one-2".to_string()));
|
||||
|
||||
// db.write_kvp("key-2".to_string(), "two".to_string())
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(db.read_kvp("key-2").unwrap(), Some("two".to_string()));
|
||||
|
||||
// db.delete_kvp("key-1".to_string()).await.unwrap();
|
||||
// assert_eq!(db.read_kvp("key-1").unwrap(), None);
|
||||
// }
|
||||
// }
|
||||
314
crates/db2/src/query.rs
Normal file
314
crates/db2/src/query.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
#[macro_export]
|
||||
macro_rules! query {
|
||||
($vis:vis fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.exec(sql_stmt)?().context(::std::format!(
|
||||
"Error in {}, exec failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt,
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident() -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.exec(sql_stmt)?().context(::std::format!(
|
||||
"Error in {}, exec failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident($arg:ident: $arg_type:ty) -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(move |connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.exec_bound::<$arg_type>(sql_stmt)?($arg)
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<()> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(move |connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.exec_bound::<($($arg_type),+)>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select::<$return_type>(sql_stmt)?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident() -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
pub async fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select::<$return_type>(sql_stmt)?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Vec<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, exec_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row::<$return_type>(sql_stmt)?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident() -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_row::<$return_type>(sql_stmt)?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<Option<$return_type:ty>> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
|
||||
self.write(move |connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row::<$return_type>(indoc! { $sql })?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis async fn $id:ident() -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_row::<$return_type>(sql_stmt)?()
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($arg:ident: $arg_type:ty) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
pub fn $id(&self, $arg: $arg_type) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row_bound::<$arg_type, $return_type>(sql_stmt)?($arg)
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis fn $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
$vis fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
self.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}
|
||||
};
|
||||
($vis:vis fn async $id:ident($($arg:ident: $arg_type:ty),+) -> Result<$return_type:ty> { $($sql:tt)+ }) => {
|
||||
$vis async fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<$return_type> {
|
||||
use $crate::anyhow::Context;
|
||||
|
||||
|
||||
self.write(|connection| {
|
||||
let sql_stmt = $crate::sqlez_macros::sql!($($sql)+);
|
||||
|
||||
connection.select_row_bound::<($($arg_type),+), $return_type>(sql_stmt)?(($($arg),+))
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound failed to execute or parse for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))?
|
||||
.context(::std::format!(
|
||||
"Error in {}, select_row_bound expected single row result but found none for: {}",
|
||||
::std::stringify!($id),
|
||||
sql_stmt
|
||||
))
|
||||
}).await
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,9 @@ util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
anyhow.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
pub mod items;
|
||||
mod project_diagnostics_settings;
|
||||
mod toolbar_controls;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeSet, HashSet};
|
||||
@@ -19,6 +21,7 @@ use language::{
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
use project_diagnostics_settings::ProjectDiagnosticsSettings;
|
||||
use serde_json::json;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
@@ -30,18 +33,21 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy]);
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
|
||||
const CONTEXT_LINE_COUNT: u32 = 1;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
settings::register::<ProjectDiagnosticsSettings>(cx);
|
||||
cx.add_action(ProjectDiagnosticsEditor::deploy);
|
||||
cx.add_action(ProjectDiagnosticsEditor::toggle_warnings);
|
||||
items::init(cx);
|
||||
}
|
||||
|
||||
@@ -55,6 +61,7 @@ struct ProjectDiagnosticsEditor {
|
||||
excerpts: ModelHandle<MultiBuffer>,
|
||||
path_states: Vec<PathState>,
|
||||
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
|
||||
include_warnings: bool,
|
||||
}
|
||||
|
||||
struct PathState {
|
||||
@@ -187,6 +194,7 @@ impl ProjectDiagnosticsEditor {
|
||||
editor,
|
||||
path_states: Default::default(),
|
||||
paths_to_update,
|
||||
include_warnings: settings::get::<ProjectDiagnosticsSettings>(cx).include_warnings,
|
||||
};
|
||||
this.update_excerpts(None, cx);
|
||||
this
|
||||
@@ -204,6 +212,18 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
|
||||
self.include_warnings = !self.include_warnings;
|
||||
self.paths_to_update = self
|
||||
.project
|
||||
.read(cx)
|
||||
.diagnostic_summaries(cx)
|
||||
.map(|(path, server_id, _)| (path, server_id))
|
||||
.collect();
|
||||
self.update_excerpts(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn update_excerpts(
|
||||
&mut self,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
@@ -277,14 +297,18 @@ impl ProjectDiagnosticsEditor {
|
||||
let mut blocks_to_add = Vec::new();
|
||||
let mut blocks_to_remove = HashSet::default();
|
||||
let mut first_excerpt_id = None;
|
||||
let max_severity = if self.include_warnings {
|
||||
DiagnosticSeverity::WARNING
|
||||
} else {
|
||||
DiagnosticSeverity::ERROR
|
||||
};
|
||||
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
|
||||
let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
|
||||
let mut new_groups = snapshot
|
||||
.diagnostic_groups(language_server_id)
|
||||
.into_iter()
|
||||
.filter(|(_, group)| {
|
||||
group.entries[group.primary_ix].diagnostic.severity
|
||||
<= DiagnosticSeverity::WARNING
|
||||
group.entries[group.primary_ix].diagnostic.severity <= max_severity
|
||||
})
|
||||
.peekable();
|
||||
loop {
|
||||
@@ -1501,6 +1525,7 @@ mod tests {
|
||||
client::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user