Compare commits
770 Commits
render-onc
...
list-scrol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fb1e6846d | ||
|
|
188d727d31 | ||
|
|
6b58eb202d | ||
|
|
4adafb1148 | ||
|
|
f3b764b7d2 | ||
|
|
e4bc346723 | ||
|
|
049314e84e | ||
|
|
d03e29d55d | ||
|
|
8feb11ccee | ||
|
|
008c56a09b | ||
|
|
cf42d95af6 | ||
|
|
60e4311294 | ||
|
|
cbbcc3502a | ||
|
|
44890d6966 | ||
|
|
2517b1017b | ||
|
|
1fecd3c327 | ||
|
|
be636348c4 | ||
|
|
13dad89a85 | ||
|
|
324aeaa0de | ||
|
|
ad05f22256 | ||
|
|
2ef3954700 | ||
|
|
650ca1f39b | ||
|
|
fce0c1f4d0 | ||
|
|
e9730e0b5c | ||
|
|
cd93ac1d2a | ||
|
|
c739906413 | ||
|
|
c7d8169cab | ||
|
|
213ed2028c | ||
|
|
6955579f19 | ||
|
|
113c7287df | ||
|
|
4353bdb9d5 | ||
|
|
40b219f861 | ||
|
|
f67d4cea31 | ||
|
|
0dc02b8354 | ||
|
|
133b1a8ad2 | ||
|
|
f0cc54a0b5 | ||
|
|
27501d2929 | ||
|
|
77c8108f9b | ||
|
|
1c850f495c | ||
|
|
1d35a815a6 | ||
|
|
79e6dedb7a | ||
|
|
c653a02be3 | ||
|
|
f4c93abad5 | ||
|
|
62155f3a88 | ||
|
|
fdde76c1a5 | ||
|
|
ab5b76e943 | ||
|
|
be6c909587 | ||
|
|
a5a0ad8b5c | ||
|
|
726d761646 | ||
|
|
9ac9532d3d | ||
|
|
63cc9e5068 | ||
|
|
0cab3de0ae | ||
|
|
21476ec51d | ||
|
|
c51cedef14 | ||
|
|
7964b35607 | ||
|
|
8987b2205c | ||
|
|
970c7b8987 | ||
|
|
a283cbaf8f | ||
|
|
0b2aa052ea | ||
|
|
52e4c577d2 | ||
|
|
4596e7a68a | ||
|
|
b66e1d2d58 | ||
|
|
6f064cfc36 | ||
|
|
260a753005 | ||
|
|
6634a5e9f6 | ||
|
|
1b6721170a | ||
|
|
63ce7cd407 | ||
|
|
40a95221ea | ||
|
|
a8a5b9524d | ||
|
|
53d77b192a | ||
|
|
53ff5ff724 | ||
|
|
d0a673ec62 | ||
|
|
7a9f764aa0 | ||
|
|
5b96ffbbd1 | ||
|
|
efb4ff816a | ||
|
|
1f51f74670 | ||
|
|
a88372dc99 | ||
|
|
f272881a6b | ||
|
|
89f62a1a69 | ||
|
|
f9d569f1b8 | ||
|
|
808a0626c0 | ||
|
|
c7e19c0bcd | ||
|
|
0b78a401de | ||
|
|
58d8c51255 | ||
|
|
5e3d0a6d03 | ||
|
|
aa34637705 | ||
|
|
901bd6ac87 | ||
|
|
445f0da4d8 | ||
|
|
d1b47b4059 | ||
|
|
f3cbce7e86 | ||
|
|
04d019ef66 | ||
|
|
d23bba5b18 | ||
|
|
282e4398a0 | ||
|
|
fa223e0c6f | ||
|
|
6a164db700 | ||
|
|
8ed04fe798 | ||
|
|
853daf953b | ||
|
|
8b9b19195d | ||
|
|
5c3b8a1af1 | ||
|
|
0ee4ad6ba0 | ||
|
|
ef4bc5e20b | ||
|
|
439df24526 | ||
|
|
1fcd006b2a | ||
|
|
7c19650a40 | ||
|
|
393be3cedf | ||
|
|
c91207b450 | ||
|
|
7ad6d489a4 | ||
|
|
794b795800 | ||
|
|
2461902086 | ||
|
|
08c4e1abdc | ||
|
|
5e5eb25aab | ||
|
|
49d30b4f67 | ||
|
|
b9a904e552 | ||
|
|
6c653b9c05 | ||
|
|
b692b4c9c2 | ||
|
|
f829120f51 | ||
|
|
2a82dff2fe | ||
|
|
646b74e0a7 | ||
|
|
75324abbb5 | ||
|
|
ea708c50f0 | ||
|
|
8839bfa1df | ||
|
|
2c2c633104 | ||
|
|
18393d1fac | ||
|
|
7842fdeb32 | ||
|
|
45f7ab876f | ||
|
|
cbd1ce64f7 | ||
|
|
9a916d71b0 | ||
|
|
a40515dbb3 | ||
|
|
edb2f60d9d | ||
|
|
02bd4fb1f1 | ||
|
|
e96197d63b | ||
|
|
d02ff42608 | ||
|
|
f798b193d0 | ||
|
|
197f355729 | ||
|
|
7a1eb54106 | ||
|
|
42a3da4ba3 | ||
|
|
84aaeb4360 | ||
|
|
da3ba35d1c | ||
|
|
2a68a6171a | ||
|
|
fffe4f51fb | ||
|
|
27e1787ce3 | ||
|
|
9e6103f863 | ||
|
|
d2362d7f12 | ||
|
|
79e0d8ce3b | ||
|
|
ee45db8a78 | ||
|
|
8fc15c05c5 | ||
|
|
256f0308ae | ||
|
|
d62b151bc0 | ||
|
|
b2a0c32148 | ||
|
|
668f5accab | ||
|
|
66b93212fe | ||
|
|
57a9f024e7 | ||
|
|
22cd62213b | ||
|
|
bbe9986f38 | ||
|
|
1abc9800af | ||
|
|
034c11cc96 | ||
|
|
0885fa67fc | ||
|
|
c8cb1140b9 | ||
|
|
c092cfbfb3 | ||
|
|
a4b271e063 | ||
|
|
110612bf26 | ||
|
|
fd31e429f5 | ||
|
|
12e7f61f62 | ||
|
|
1a747c8457 | ||
|
|
2d18b949ad | ||
|
|
46c998ca8d | ||
|
|
a8d96790cf | ||
|
|
b72c54fc31 | ||
|
|
89c8a7c242 | ||
|
|
41fc30f62e | ||
|
|
f4c7b13397 | ||
|
|
f7c995c4a0 | ||
|
|
cd818f580c | ||
|
|
dba94c5122 | ||
|
|
dbb501d7eb | ||
|
|
d711087529 | ||
|
|
ebddb612c8 | ||
|
|
62c41e226b | ||
|
|
b94c335605 | ||
|
|
6bbb1642b8 | ||
|
|
e9dcca7712 | ||
|
|
1ec81e02da | ||
|
|
bcdefb8ec8 | ||
|
|
5644815c4c | ||
|
|
9e1d797445 | ||
|
|
ec798e6574 | ||
|
|
d7473ad6e7 | ||
|
|
5ebf1d9e4f | ||
|
|
f76e1cfd91 | ||
|
|
489c25ac6a | ||
|
|
ac07e230fa | ||
|
|
147c99f1a7 | ||
|
|
02a6a2e1a3 | ||
|
|
4863c9ac25 | ||
|
|
2c350a71fe | ||
|
|
fdd64832e7 | ||
|
|
eff925cb6a | ||
|
|
7e2ff63270 | ||
|
|
cc9eff89f5 | ||
|
|
e4884f1d76 | ||
|
|
02e7a06cbb | ||
|
|
5ae2060783 | ||
|
|
14def2a1a3 | ||
|
|
7c9e2f6b7d | ||
|
|
f6a7a6c4d4 | ||
|
|
2aee3e3192 | ||
|
|
5e558e2a58 | ||
|
|
80f315106d | ||
|
|
80c8fd1f4c | ||
|
|
886ec79d58 | ||
|
|
c8ddc95caa | ||
|
|
8f1c74b8bc | ||
|
|
0ced310f93 | ||
|
|
74e3b12a26 | ||
|
|
cf971f706c | ||
|
|
b5c790ed29 | ||
|
|
6549a9a091 | ||
|
|
3b8c566f31 | ||
|
|
f833cd7c16 | ||
|
|
e9002ab10a | ||
|
|
a1c8f01ff3 | ||
|
|
54ea7257ff | ||
|
|
d09dfe01f5 | ||
|
|
3c1bf0260c | ||
|
|
cb0880cf3c | ||
|
|
a58f393458 | ||
|
|
0ef97edd6e | ||
|
|
1f538c5fdd | ||
|
|
5f172a52a4 | ||
|
|
1b0ec82caa | ||
|
|
7998e8281c | ||
|
|
3f9fe58c48 | ||
|
|
9eb98122ec | ||
|
|
1de02cf6e5 | ||
|
|
b29cea287b | ||
|
|
735f2029e9 | ||
|
|
5660c8f655 | ||
|
|
ed31d36ac1 | ||
|
|
d8757845a9 | ||
|
|
2ee0ecb677 | ||
|
|
d2fe9f8f9b | ||
|
|
c9dd6b879c | ||
|
|
65bb05af4c | ||
|
|
82534b6612 | ||
|
|
79567d1c87 | ||
|
|
5b6fd3ac1d | ||
|
|
a27db35ff7 | ||
|
|
02e507b973 | ||
|
|
f2faa70f73 | ||
|
|
863222edc5 | ||
|
|
fdc2d7ce42 | ||
|
|
dec4cfa0b4 | ||
|
|
78e1c0f9c3 | ||
|
|
631e264e3c | ||
|
|
ab140ee4c2 | ||
|
|
fc9b662042 | ||
|
|
8141f4fd86 | ||
|
|
e0ca7f844a | ||
|
|
4c4b235b13 | ||
|
|
dc7e4a4b17 | ||
|
|
27703a3279 | ||
|
|
7b4b068230 | ||
|
|
ceb5d0c69e | ||
|
|
38d41acf9b | ||
|
|
d86da04584 | ||
|
|
5e79807f6f | ||
|
|
a80b145d0d | ||
|
|
ce8bdb354b | ||
|
|
dccdcd3221 | ||
|
|
8d57d6ca6f | ||
|
|
e534c5fdcd | ||
|
|
eed5a698cf | ||
|
|
412c6157b1 | ||
|
|
dffe0ea058 | ||
|
|
2299730538 | ||
|
|
ede86d9187 | ||
|
|
c9b50c8bab | ||
|
|
7c5df51d2e | ||
|
|
30725d6a17 | ||
|
|
09db455db2 | ||
|
|
c2d3e74ffb | ||
|
|
62bfa3b452 | ||
|
|
16b5d4b35c | ||
|
|
20352c51c1 | ||
|
|
f9efaebddf | ||
|
|
af72772a72 | ||
|
|
b73ccc8180 | ||
|
|
d433da1e70 | ||
|
|
001ce47a0c | ||
|
|
e5616bce98 | ||
|
|
92fbdb429c | ||
|
|
b43dc480a9 | ||
|
|
2c2c14a360 | ||
|
|
dffe323e73 | ||
|
|
f0ca7141b8 | ||
|
|
1f6c69c7dc | ||
|
|
590238bcca | ||
|
|
caa5fccbc4 | ||
|
|
1dd6625dd4 | ||
|
|
ee695bbb34 | ||
|
|
9695ea1017 | ||
|
|
e0ec5032e9 | ||
|
|
948c065f86 | ||
|
|
ae6ddceb67 | ||
|
|
eff3a72fb5 | ||
|
|
591dc9d82a | ||
|
|
71a1125e88 | ||
|
|
959b2961ff | ||
|
|
45992b0d63 | ||
|
|
9162f299a7 | ||
|
|
2c5603032d | ||
|
|
63667ecf6f | ||
|
|
13bb16577c | ||
|
|
1c9b984738 | ||
|
|
7db0a9e105 | ||
|
|
fc16e4509a | ||
|
|
c82fea375d | ||
|
|
5bdaf0e074 | ||
|
|
c10d8a8110 | ||
|
|
79773178c8 | ||
|
|
2c2e5144c9 | ||
|
|
059959a4ab | ||
|
|
3627ff87f0 | ||
|
|
9a2fd18425 | ||
|
|
4cb4033a36 | ||
|
|
b212aab00d | ||
|
|
584a3a7627 | ||
|
|
63213b5a57 | ||
|
|
4a5f703c32 | ||
|
|
a3e4559b0e | ||
|
|
26c797c358 | ||
|
|
fb352af35c | ||
|
|
4f4029524f | ||
|
|
e928ed44a0 | ||
|
|
23626aa902 | ||
|
|
5ab6874ae9 | ||
|
|
4ee4e4e8d8 | ||
|
|
7a0aa1e5df | ||
|
|
b6ed3b258c | ||
|
|
50fe683305 | ||
|
|
b5924d6b11 | ||
|
|
80ae640060 | ||
|
|
0af0c5549c | ||
|
|
68d309e79c | ||
|
|
fd2f1c2594 | ||
|
|
2108ddf621 | ||
|
|
ee23fa43dc | ||
|
|
118c9b5fe8 | ||
|
|
b9a917f42a | ||
|
|
7dc22fef24 | ||
|
|
24b08921fe | ||
|
|
dfe4fc4d0a | ||
|
|
11c16258eb | ||
|
|
ff734d494f | ||
|
|
3b1a0652ae | ||
|
|
b3e741b397 | ||
|
|
1a5f6f604b | ||
|
|
0f7fc8c1a0 | ||
|
|
9ffe78d264 | ||
|
|
45230dcaf3 | ||
|
|
aa3c9b8568 | ||
|
|
859f2d2862 | ||
|
|
63b65b2b2e | ||
|
|
1da18ebe9a | ||
|
|
6426997abb | ||
|
|
6a5b5f022d | ||
|
|
2de6758443 | ||
|
|
fe83919746 | ||
|
|
6730ff3624 | ||
|
|
b0f9144167 | ||
|
|
0edd89a92f | ||
|
|
6f7995c150 | ||
|
|
379ba620b2 | ||
|
|
2bf48872b6 | ||
|
|
03ebf0a5a9 | ||
|
|
26ae31b503 | ||
|
|
6b0457c52f | ||
|
|
d81fb3680e | ||
|
|
d788bf3f1e | ||
|
|
c07455efa7 | ||
|
|
d85db44552 | ||
|
|
89aa6a3726 | ||
|
|
f5679f98d6 | ||
|
|
be509a5ce0 | ||
|
|
5dd9db3607 | ||
|
|
aace0d816d | ||
|
|
4479628342 | ||
|
|
0a1765b01f | ||
|
|
c3e7732eab | ||
|
|
a985b7aab4 | ||
|
|
20ae58eddd | ||
|
|
9408eecb6e | ||
|
|
2ab84b81da | ||
|
|
e821e1fc35 | ||
|
|
237efc841e | ||
|
|
4b23c5c658 | ||
|
|
1c52b936bc | ||
|
|
4f507756d6 | ||
|
|
12b58f5b60 | ||
|
|
f09d9ef723 | ||
|
|
be5eb3e3b5 | ||
|
|
53f3f960d2 | ||
|
|
6f9e08ceea | ||
|
|
85d72f63c1 | ||
|
|
4c4ec221af | ||
|
|
910aeb4f56 | ||
|
|
0d33f01fa4 | ||
|
|
e0ccaa60ff | ||
|
|
a40a5fb212 | ||
|
|
aed11ee8cb | ||
|
|
13f4cc563c | ||
|
|
ab75dbe7af | ||
|
|
88188b0c30 | ||
|
|
ce16e5b54a | ||
|
|
134f3eb79f | ||
|
|
5fefc121bf | ||
|
|
9fedb4c7ee | ||
|
|
f0c205be5d | ||
|
|
f0bc4a04bd | ||
|
|
1abc876c15 | ||
|
|
8e4f2fb25a | ||
|
|
faa896343b | ||
|
|
a8a5785ec5 | ||
|
|
c9be637b6b | ||
|
|
0b7e324d86 | ||
|
|
8c59a2d2c0 | ||
|
|
21d3ae603d | ||
|
|
1b07fd6a70 | ||
|
|
a75f1a4a95 | ||
|
|
eef6c3729e | ||
|
|
9849a0a6d8 | ||
|
|
6d62e6c562 | ||
|
|
0441b4805a | ||
|
|
e1c8369b3d | ||
|
|
4f74d65d0c | ||
|
|
ebbbeca9a6 | ||
|
|
cb11c1282c | ||
|
|
a003a91212 | ||
|
|
bac39e85a8 | ||
|
|
e228422f14 | ||
|
|
8b7be8f614 | ||
|
|
e5a5b1e84c | ||
|
|
5098fafa02 | ||
|
|
f987a640fd | ||
|
|
c63ca09eed | ||
|
|
164084c61c | ||
|
|
180ba42456 | ||
|
|
bd6fa66a7c | ||
|
|
16dc978bb4 | ||
|
|
f922ad9f7f | ||
|
|
39887be7d0 | ||
|
|
d516ae0d8a | ||
|
|
3ffcb97da2 | ||
|
|
e78538e162 | ||
|
|
865baaa1a1 | ||
|
|
5fdfdb046c | ||
|
|
5eb89781e3 | ||
|
|
b34b197d89 | ||
|
|
679851e349 | ||
|
|
8d4652a4db | ||
|
|
21dfe58ad9 | ||
|
|
75afb65b26 | ||
|
|
0a8a84f656 | ||
|
|
0e1597d385 | ||
|
|
428c517693 | ||
|
|
81098312ef | ||
|
|
5243401afa | ||
|
|
591299b4c7 | ||
|
|
d9b8c07b1e | ||
|
|
e20309f560 | ||
|
|
daf6201deb | ||
|
|
b64fe04803 | ||
|
|
82b3efa16c | ||
|
|
cdae8ebd06 | ||
|
|
1b39916438 | ||
|
|
091d93533d | ||
|
|
69bfd47cf9 | ||
|
|
7b76db4b50 | ||
|
|
74a5ae5633 | ||
|
|
fac029b808 | ||
|
|
f052329a6b | ||
|
|
d2c1897385 | ||
|
|
14993e0876 | ||
|
|
624ea58f2e | ||
|
|
cc0bc444b1 | ||
|
|
f9cd45269a | ||
|
|
02174084ca | ||
|
|
90b8acfb34 | ||
|
|
4fbbb28da2 | ||
|
|
54cfcef0aa | ||
|
|
a37f86f294 | ||
|
|
92d2048aa4 | ||
|
|
8638fc4149 | ||
|
|
012543052b | ||
|
|
1a1451a943 | ||
|
|
b4a5701e12 | ||
|
|
e49325080c | ||
|
|
c95a7c7387 | ||
|
|
bda1dc7721 | ||
|
|
9d53287341 | ||
|
|
481e42ade9 | ||
|
|
27a6761afc | ||
|
|
4c27f4453c | ||
|
|
04bbd107c1 | ||
|
|
b357ae4dc3 | ||
|
|
cd60d466b1 | ||
|
|
0eef4d03ed | ||
|
|
0b8f9067ff | ||
|
|
4f040208c6 | ||
|
|
9451bd605d | ||
|
|
3bdd51cb2a | ||
|
|
4ebae4d8bd | ||
|
|
df5de47a78 | ||
|
|
2bd428102b | ||
|
|
fc2251e7ed | ||
|
|
fb377aed73 | ||
|
|
d8ed7c0721 | ||
|
|
6ab3825ab1 | ||
|
|
87cf0cf5ac | ||
|
|
0d31722589 | ||
|
|
3c05d57e6a | ||
|
|
509504d4d2 | ||
|
|
139708d486 | ||
|
|
7e5aa5ce77 | ||
|
|
e77846d2dc | ||
|
|
e377bd805b | ||
|
|
6f5cc0af94 | ||
|
|
e99fa77bda | ||
|
|
41e7653906 | ||
|
|
c7b79c9aef | ||
|
|
390b0d8d56 | ||
|
|
ee260a5e24 | ||
|
|
960ef7116d | ||
|
|
3e2c517dd1 | ||
|
|
dbfc7d3555 | ||
|
|
c23a610d52 | ||
|
|
233aac5573 | ||
|
|
043cef89eb | ||
|
|
ac34229118 | ||
|
|
912c30c05b | ||
|
|
97e6fd295a | ||
|
|
a8bf0834e6 | ||
|
|
a4035f15af | ||
|
|
35481e2c79 | ||
|
|
5d59108b97 | ||
|
|
f735f5287e | ||
|
|
a62c4845a9 | ||
|
|
d92153218c | ||
|
|
3e3b64bb1c | ||
|
|
3796e7eecb | ||
|
|
6e44f53ea1 | ||
|
|
f1314afe35 | ||
|
|
96f6b89508 | ||
|
|
acd1aec862 | ||
|
|
64259e4a0b | ||
|
|
43d28cc0c1 | ||
|
|
465e53ef41 | ||
|
|
46ac82f498 | ||
|
|
938f2531c4 | ||
|
|
eab3476304 | ||
|
|
e7e56757dc | ||
|
|
c288c6eaf9 | ||
|
|
d010f5f98d | ||
|
|
5fbc60d8da | ||
|
|
396ecefa4f | ||
|
|
0d4839b973 | ||
|
|
e36c7dd301 | ||
|
|
9e7a90a3d4 | ||
|
|
db5ded0252 | ||
|
|
8d1518d70c | ||
|
|
77acba9e4c | ||
|
|
fbe440250e | ||
|
|
6bf7ad71eb | ||
|
|
7677998470 | ||
|
|
60ce75c34a | ||
|
|
a41c857855 | ||
|
|
ed8e62cd18 | ||
|
|
4c2348eb53 | ||
|
|
d927c2f497 | ||
|
|
bcf449d3fe | ||
|
|
af3fa4ec0b | ||
|
|
75891e83f3 | ||
|
|
b13638fa76 | ||
|
|
450f2bf6b6 | ||
|
|
5666066db4 | ||
|
|
ee027bc112 | ||
|
|
a85e0db1f4 | ||
|
|
8ee84249ec | ||
|
|
64376ad37a | ||
|
|
d8fd422cf4 | ||
|
|
b24a3b8fcb | ||
|
|
525fe70de6 | ||
|
|
21755c7d20 | ||
|
|
3855413725 | ||
|
|
40bbbd3226 | ||
|
|
070674a4fd | ||
|
|
a9cb6589dd | ||
|
|
874fde09ab | ||
|
|
f33cd3d463 | ||
|
|
132ad5989c | ||
|
|
01d3432a40 | ||
|
|
9411898720 | ||
|
|
4a01726e5e | ||
|
|
a761e6ca0e | ||
|
|
5451db9c96 | ||
|
|
3ac545088a | ||
|
|
179ad03ebc | ||
|
|
ecb3bd7f59 | ||
|
|
600b564bbf | ||
|
|
1ee109cec7 | ||
|
|
63bd4ac999 | ||
|
|
9fb3cb6a69 | ||
|
|
6c37393dd1 | ||
|
|
ca0dcf741f | ||
|
|
17b5f9294c | ||
|
|
26121713b3 | ||
|
|
eac4b2d076 | ||
|
|
8faa1f6e58 | ||
|
|
a760508080 | ||
|
|
84ee833d0c | ||
|
|
2a8f4f19d6 | ||
|
|
979ff70196 | ||
|
|
ca2052b0c1 | ||
|
|
eca49ef02e | ||
|
|
f3d142b7e9 | ||
|
|
4a25fae51e | ||
|
|
369803cbc8 | ||
|
|
6468fe737e | ||
|
|
e6aab57656 | ||
|
|
ccfc4fc0b9 | ||
|
|
47b4d9942f | ||
|
|
107c3d7f67 | ||
|
|
1c62abbf79 | ||
|
|
d551b41aae | ||
|
|
f8614b5909 | ||
|
|
b0d9e3c8fa | ||
|
|
24f3034106 | ||
|
|
52119ca203 | ||
|
|
6f839a1b48 | ||
|
|
4f885252ab | ||
|
|
19ecccb107 | ||
|
|
feb7753a73 | ||
|
|
e31a8f0536 | ||
|
|
82f6f77117 | ||
|
|
212d9254e1 | ||
|
|
b48b74559c | ||
|
|
7a8aba329b | ||
|
|
1c2f906e48 | ||
|
|
e9e621518f | ||
|
|
3ea12ad0d7 | ||
|
|
fc4b621b32 | ||
|
|
8c53f1b9c2 | ||
|
|
1e6214440d | ||
|
|
b2b5df4428 | ||
|
|
a5951df21f | ||
|
|
ab83f4319b | ||
|
|
fd165206bf | ||
|
|
8a35a02863 | ||
|
|
1acc6b462f | ||
|
|
cd4ea344a6 | ||
|
|
8ca9f4e12a | ||
|
|
30af3ffaf3 | ||
|
|
44876062c9 | ||
|
|
56d3d663a5 | ||
|
|
0d1d01687b | ||
|
|
3ba5dbb9e8 | ||
|
|
a71180257d | ||
|
|
c40ab3c3da | ||
|
|
bf4211b03a | ||
|
|
eb647be685 | ||
|
|
714b45157b | ||
|
|
9590f253a9 | ||
|
|
5cbe8deb50 | ||
|
|
4c1514edc4 | ||
|
|
fad1f8db96 | ||
|
|
039c933d8e | ||
|
|
2e61a84bd6 | ||
|
|
48a9f2be4c | ||
|
|
682712f132 | ||
|
|
047cfe5108 | ||
|
|
f227c3284d | ||
|
|
481c19fbaf | ||
|
|
0baa9a782b | ||
|
|
d31b53b912 | ||
|
|
d965ddba27 | ||
|
|
6a3925c3e1 | ||
|
|
31565a810d | ||
|
|
6ebe5d5053 | ||
|
|
e5b6b0ee9e | ||
|
|
3c46e81533 | ||
|
|
7e61d340ce | ||
|
|
19bfed165b | ||
|
|
54357d6553 | ||
|
|
bf39968105 | ||
|
|
700168467e | ||
|
|
e19860d3b0 | ||
|
|
bc2dfcfa8e | ||
|
|
879a069b35 | ||
|
|
31a4acf98a | ||
|
|
95a413847a | ||
|
|
4a060db801 | ||
|
|
22e6803fc6 | ||
|
|
18d3b1c397 | ||
|
|
f2b62c3946 | ||
|
|
510320bb47 | ||
|
|
7eeb8078f6 | ||
|
|
1ad22231d2 | ||
|
|
df7b89b6cb | ||
|
|
204218b5f5 | ||
|
|
56d043f671 | ||
|
|
41b7acc1de | ||
|
|
e754c6626d | ||
|
|
2a2b3b5e91 | ||
|
|
b463454062 | ||
|
|
390ecb5b0c | ||
|
|
c191943849 | ||
|
|
72a1ada0ae | ||
|
|
0b67983ddf | ||
|
|
2cc1df9053 | ||
|
|
3b918bfee8 | ||
|
|
cee6fd8dd3 | ||
|
|
eee63835fb | ||
|
|
c2751c717e | ||
|
|
f33c0e80a6 | ||
|
|
35f35dd476 | ||
|
|
575ab81409 | ||
|
|
a876b6f700 | ||
|
|
88af458f62 | ||
|
|
f01a04a8e0 | ||
|
|
ebeb0fda28 | ||
|
|
6e84d8fbc0 | ||
|
|
eb74ad7caa | ||
|
|
6da57cbc6e | ||
|
|
37e3cc1291 | ||
|
|
0def2bc0d2 | ||
|
|
c04f123e44 | ||
|
|
ee2b6834bd | ||
|
|
c86e99928b | ||
|
|
6e4268a471 | ||
|
|
62b1843704 | ||
|
|
f0c7b3e6ee | ||
|
|
2611b5d6d7 | ||
|
|
b45234eece | ||
|
|
fa74c49dbb | ||
|
|
9abce4bdd9 | ||
|
|
10c4df20e9 | ||
|
|
524f892fb0 | ||
|
|
7b0b87380d | ||
|
|
cac6e22e8e | ||
|
|
fd5793ddec | ||
|
|
031fca4105 | ||
|
|
2c8d243d22 | ||
|
|
eaf90a4fbd | ||
|
|
71e9bd8fa3 | ||
|
|
566857b0b7 | ||
|
|
92953fb53d | ||
|
|
a4a1e6ba98 | ||
|
|
469b05684f | ||
|
|
7e7a778d11 | ||
|
|
e557eb4afe | ||
|
|
663bbb06d9 | ||
|
|
abe5a9c85f | ||
|
|
ebccdb64bc | ||
|
|
170291ff96 | ||
|
|
daddb03e7a | ||
|
|
a0dcc9618e | ||
|
|
db3f487474 | ||
|
|
0926db9111 | ||
|
|
35e0b53639 | ||
|
|
176a68f90f |
12
.cargo/ci-config.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
# This config is different from config.toml in this directory, as the latter is recognized by Cargo.
|
||||
# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml
|
||||
# with preference for settings from Zeds config.toml.
|
||||
# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence.
|
||||
# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure
|
||||
# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file
|
||||
# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml`
|
||||
# would be incovenient.
|
||||
# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables
|
||||
# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it.
|
||||
[build]
|
||||
rustflags = ["-D", "warnings"]
|
||||
265
.github/workflows/ci.yml
vendored
@@ -1,146 +1,149 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
|
||||
concurrency:
|
||||
# Allow only one workflow per any non-`main` branch.
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: printf "[build]\nrustflags = [\"-D\", \"warnings\"]" > $HOME/.cargo/config.toml
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Set up default .cargo/config.toml
|
||||
run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
# TODO kb seems that zed.dev relies on GitHub releases for release version tracking.
|
||||
# Find alternatives for `nightly` or just go on with more releases?
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch or specific label
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
name: Upload app bundle to release
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
files: target/release/Zed.dmg
|
||||
body: ""
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
160
.github/workflows/release_nightly.yml
vendored
@@ -1,98 +1,98 @@
|
||||
name: Release Nightly
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
schedule:
|
||||
# Fire every night at 1:00am
|
||||
- cron: "0 1 * * *"
|
||||
push:
|
||||
tags:
|
||||
- "nightly"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
- name: Run rustfmt
|
||||
uses: ./.github/actions/check_formatting
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
- name: Run tests
|
||||
uses: ./.github/actions/run_tests
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }}
|
||||
APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }}
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }}
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
rustup target add aarch64-apple-darwin
|
||||
rustup target add x86_64-apple-darwin
|
||||
rustup target add wasm32-wasi
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
clean: false
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
- name: Set release channel to nightly
|
||||
run: |
|
||||
set -eu
|
||||
version=$(git rev-parse --short HEAD)
|
||||
echo "Publishing version: ${version} on release channel nightly"
|
||||
echo "nightly" > crates/zed/RELEASE_CHANNEL
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
- name: Create app bundle
|
||||
run: script/bundle -2
|
||||
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
- name: Upload Zed Nightly
|
||||
run: script/upload-nightly
|
||||
|
||||
407
Cargo.lock
generated
@@ -19,6 +19,25 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "activity_indicator2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_update2",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"project2",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.17.0"
|
||||
@@ -293,6 +312,15 @@ version = "1.0.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.7"
|
||||
@@ -357,6 +385,47 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"client2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"fs2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"indoc",
|
||||
"isahc",
|
||||
"language2",
|
||||
"log",
|
||||
"menu2",
|
||||
"multi_buffer2",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot 0.11.2",
|
||||
"project2",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"tiktoken-rs",
|
||||
"ui2",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.4.1"
|
||||
@@ -394,9 +463,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.3.15"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a"
|
||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"futures-core",
|
||||
@@ -1076,6 +1145,24 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "breadcrumbs2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"editor2",
|
||||
"gpui2",
|
||||
"itertools 0.10.5",
|
||||
"language2",
|
||||
"outline2",
|
||||
"project2",
|
||||
"search2",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bromberg_sl2"
|
||||
version = "0.6.0"
|
||||
@@ -1192,6 +1279,7 @@ dependencies = [
|
||||
"fs2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"image",
|
||||
"language2",
|
||||
"live_kit_client2",
|
||||
"log",
|
||||
@@ -1203,6 +1291,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -1299,10 +1388,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.84"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -1664,7 +1754,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.28.0"
|
||||
version = "0.30.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1751,7 +1841,7 @@ dependencies = [
|
||||
"clap 3.2.25",
|
||||
"client2",
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collab_ui2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"dashmap",
|
||||
@@ -1880,6 +1970,7 @@ dependencies = [
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project2",
|
||||
"recent_projects2",
|
||||
"rich_text2",
|
||||
"rpc2",
|
||||
"schemars",
|
||||
@@ -2074,12 +2165,13 @@ dependencies = [
|
||||
"lsp2",
|
||||
"node_runtime",
|
||||
"parking_lot 0.11.2",
|
||||
"rpc",
|
||||
"rpc2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -2102,6 +2194,25 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copilot_button2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"copilot2",
|
||||
"editor2",
|
||||
"fs2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"util",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
@@ -3062,6 +3173,12 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-srgb8"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
@@ -3123,6 +3240,39 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "feedback2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
"db2",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"gpui2",
|
||||
"human_bytes",
|
||||
"isahc",
|
||||
"language2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"menu2",
|
||||
"postage",
|
||||
"project2",
|
||||
"regex",
|
||||
"search2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"settings2",
|
||||
"smallvec",
|
||||
"sysinfo",
|
||||
"theme2",
|
||||
"tree-sitter-markdown",
|
||||
"ui2",
|
||||
"urlencoding",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-per-thread-logger"
|
||||
version = "0.1.6"
|
||||
@@ -4521,6 +4671,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "journal"
|
||||
version = "0.1.0"
|
||||
@@ -4750,6 +4909,24 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_selector2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"picker2",
|
||||
"project2",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_tools"
|
||||
version = "0.1.0"
|
||||
@@ -4878,18 +5055,18 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "linkme"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
|
||||
checksum = "b1e6b0bb9ca88d3c5ae88240beb9683821f903b824ee8381ef9ab4e8522fbfa9"
|
||||
dependencies = [
|
||||
"linkme-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkme-impl"
|
||||
version = "0.3.17"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
|
||||
checksum = "b3b3f61e557a617ec6ba36c79431e1f3b5e100d67cfbdb61ed6ef384298af016"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6086,12 +6263,54 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "outline2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"ordered-float 2.10.0",
|
||||
"picker2",
|
||||
"postage",
|
||||
"settings2",
|
||||
"smol",
|
||||
"text2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2e2f34147767aa758aa649415b50a69eeb46a67f9dc7db8011eeb3d84b351dc"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"fast-srgb8",
|
||||
"palette_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_derive"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7db010ec5ff3d4385e4f133916faacd9dad0f6a09394c92d825b3aed310fa0a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.37",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parity-tokio-ipc"
|
||||
version = "0.9.0"
|
||||
@@ -6976,6 +7195,18 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick_action_bar2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"assistant2",
|
||||
"editor2",
|
||||
"gpui2",
|
||||
"search2",
|
||||
"ui2",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
@@ -7158,6 +7389,28 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "recent_projects2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"db",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"ordered-float 2.10.0",
|
||||
"picker2",
|
||||
"postage",
|
||||
"settings2",
|
||||
"smol",
|
||||
"text2",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.16"
|
||||
@@ -8137,6 +8390,57 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semantic_index2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"client2",
|
||||
"collections",
|
||||
"ctor",
|
||||
"env_logger 0.9.3",
|
||||
"futures 0.3.28",
|
||||
"globset",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"ndarray",
|
||||
"node_runtime",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project2",
|
||||
"rand 0.8.5",
|
||||
"rpc2",
|
||||
"rusqlite",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings2",
|
||||
"sha1",
|
||||
"smol",
|
||||
"tempdir",
|
||||
"tiktoken-rs",
|
||||
"tree-sitter",
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-lua",
|
||||
"tree-sitter-php",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.18"
|
||||
@@ -9332,6 +9636,7 @@ dependencies = [
|
||||
"terminal2",
|
||||
"theme2",
|
||||
"thiserror",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
@@ -9438,6 +9743,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"json_comments",
|
||||
"log",
|
||||
"palette",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"simplelog",
|
||||
@@ -9466,6 +9772,27 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "theme_selector2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor2",
|
||||
"feature_flags2",
|
||||
"fs2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
"picker2",
|
||||
"postage",
|
||||
"settings2",
|
||||
"smol",
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.48"
|
||||
@@ -9925,7 +10252,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.20.10"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=b5f461a69bf3df7298b1903574d506179e6390b0#b5f461a69bf3df7298b1903574d506179e6390b0"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -10173,10 +10500,19 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-uiua"
|
||||
version = "0.3.3"
|
||||
source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-vue"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58#9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-vue?rev=6608d9d60c386f19d80af7d8132322fa11199c42#6608d9d60c386f19d80af7d8132322fa11199c42"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
@@ -11040,6 +11376,31 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "welcome2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client2",
|
||||
"db2",
|
||||
"editor2",
|
||||
"fs2",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"install_cli2",
|
||||
"log",
|
||||
"picker2",
|
||||
"project2",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings2",
|
||||
"theme2",
|
||||
"theme_selector2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "4.4.2"
|
||||
@@ -11493,7 +11854,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.114.0"
|
||||
version = "0.117.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
@@ -11610,6 +11971,7 @@ dependencies = [
|
||||
"tree-sitter-svelte",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-uiua",
|
||||
"tree-sitter-vue",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
@@ -11633,17 +11995,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed2"
|
||||
version = "0.109.0"
|
||||
version = "2.0.0"
|
||||
dependencies = [
|
||||
"activity_indicator2",
|
||||
"ai2",
|
||||
"anyhow",
|
||||
"assistant2",
|
||||
"async-compression",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"audio2",
|
||||
"auto_update2",
|
||||
"backtrace",
|
||||
"breadcrumbs2",
|
||||
"call2",
|
||||
"channel2",
|
||||
"chrono",
|
||||
"cli",
|
||||
"client2",
|
||||
@@ -11651,12 +12018,14 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette2",
|
||||
"copilot2",
|
||||
"copilot_button2",
|
||||
"ctor",
|
||||
"db2",
|
||||
"diagnostics2",
|
||||
"editor2",
|
||||
"env_logger 0.9.3",
|
||||
"feature_flags2",
|
||||
"feedback2",
|
||||
"file_finder2",
|
||||
"fs2",
|
||||
"fsevent",
|
||||
@@ -11670,6 +12039,7 @@ dependencies = [
|
||||
"isahc",
|
||||
"journal2",
|
||||
"language2",
|
||||
"language_selector2",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -11677,11 +12047,14 @@ dependencies = [
|
||||
"menu2",
|
||||
"node_runtime",
|
||||
"num_cpus",
|
||||
"outline2",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"project2",
|
||||
"project_panel2",
|
||||
"quick_action_bar2",
|
||||
"rand 0.8.5",
|
||||
"recent_projects2",
|
||||
"regex",
|
||||
"rope2",
|
||||
"rpc2",
|
||||
@@ -11689,6 +12062,7 @@ dependencies = [
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"search2",
|
||||
"semantic_index2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
@@ -11702,6 +12076,7 @@ dependencies = [
|
||||
"terminal_view2",
|
||||
"text2",
|
||||
"theme2",
|
||||
"theme_selector2",
|
||||
"thiserror",
|
||||
"tiny_http",
|
||||
"toml 0.5.11",
|
||||
@@ -11731,6 +12106,7 @@ dependencies = [
|
||||
"tree-sitter-svelte",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-uiua",
|
||||
"tree-sitter-vue",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
@@ -11738,6 +12114,7 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"welcome2",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
]
|
||||
|
||||
22
Cargo.toml
@@ -1,13 +1,16 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/activity_indicator2",
|
||||
"crates/ai",
|
||||
"crates/assistant",
|
||||
"crates/assistant2",
|
||||
"crates/audio",
|
||||
"crates/audio2",
|
||||
"crates/auto_update",
|
||||
"crates/auto_update2",
|
||||
"crates/breadcrumbs",
|
||||
"crates/breadcrumbs2",
|
||||
"crates/call",
|
||||
"crates/call2",
|
||||
"crates/channel",
|
||||
@@ -59,6 +62,7 @@ members = [
|
||||
"crates/language",
|
||||
"crates/language2",
|
||||
"crates/language_selector",
|
||||
"crates/language_selector2",
|
||||
"crates/language_tools",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
@@ -73,6 +77,7 @@ members = [
|
||||
"crates/notifications",
|
||||
"crates/notifications2",
|
||||
"crates/outline",
|
||||
"crates/outline2",
|
||||
"crates/picker",
|
||||
"crates/picker2",
|
||||
"crates/plugin",
|
||||
@@ -85,12 +90,16 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_panel2",
|
||||
"crates/project_symbols",
|
||||
"crates/quick_action_bar2",
|
||||
"crates/recent_projects",
|
||||
"crates/recent_projects2",
|
||||
"crates/rope",
|
||||
"crates/rpc",
|
||||
"crates/rpc2",
|
||||
"crates/search",
|
||||
"crates/search2",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_index2",
|
||||
"crates/settings",
|
||||
"crates/settings2",
|
||||
"crates/snippet",
|
||||
@@ -107,14 +116,15 @@ members = [
|
||||
"crates/theme2",
|
||||
"crates/theme_importer",
|
||||
"crates/theme_selector",
|
||||
"crates/theme_selector2",
|
||||
"crates/ui2",
|
||||
"crates/util",
|
||||
"crates/semantic_index",
|
||||
"crates/story",
|
||||
"crates/vim",
|
||||
"crates/vcs_menu",
|
||||
"crates/workspace2",
|
||||
"crates/welcome",
|
||||
"crates/welcome2",
|
||||
"crates/xtask",
|
||||
"crates/zed",
|
||||
"crates/zed2",
|
||||
@@ -127,6 +137,7 @@ resolver = "2"
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.57" }
|
||||
async-trait = { version = "0.1" }
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
# TODO: Switch back to the published version of `ctor` once:
|
||||
# 1. A new version of `ctor` is published with this change: https://github.com/mmastrac/rust-ctor/pull/295
|
||||
# 2. We've confirmed it's fine to update to the latest version of `ctor` (we're currently on v0.1.20).
|
||||
@@ -194,9 +205,11 @@ tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml",
|
||||
tree-sitter-lua = "0.0.14"
|
||||
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
|
||||
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
|
||||
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "6608d9d60c386f19d80af7d8132322fa11199c42"}
|
||||
tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "b5f461a69bf3df7298b1903574d506179e6390b0" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
@@ -208,11 +221,12 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
debug = "limited"
|
||||
|
||||
[profile.dev.package.taffy]
|
||||
opt-level = 3
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
debug = "limited"
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
4
Procfile.zed2
Normal file
@@ -0,0 +1,4 @@
|
||||
web: cd ../zed.dev && PORT=3000 npm run dev
|
||||
collab: cd crates/collab2 && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
|
||||
livekit: livekit-server --dev
|
||||
postgrest: postgrest crates/collab2/admin_api.conf
|
||||
3
assets/icons/arrow_down.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.00001 12L3.5 7.50001M8.00001 12L12.5 7.50001M8.00001 12L8.00001 3.00001" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 272 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.125 6.99344L6.35938 3.63281M3.125 6.99344L6.35938 10.3672M3.125 6.99344H11" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 7.50001L8 3M3.5 7.50001L8 12M3.5 7.50001H12.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 275 B After Width: | Height: | Size: 248 B |
@@ -1,3 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.8906 7.00125L7.64062 3.64062M10.8906 7.00125L7.64062 10.375M10.8906 7.00125H3" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12.5 7.5L8 12M12.5 7.5L8 3M12.5 7.5L3.5 7.5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 279 B After Width: | Height: | Size: 242 B |
3
assets/icons/arrow_up.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.99999 3.00001L12.5 7.50001M7.99999 3.00001L3.49999 7.50001M7.99999 3.00001L7.99999 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
3
assets/icons/command.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 3.625C11 2.86561 11.6156 2.25 12.375 2.25C13.1344 2.25 13.75 2.86561 13.75 3.625C13.75 4.38401 13.135 4.99939 12.3761 5C12.3758 5 12.3754 5 12.375 5H11V3.625ZM9.75 5V3.625C9.75 2.17525 10.9253 1 12.375 1C13.8247 1 15 2.17525 15 3.625C15 4.98872 13.9601 6.10955 12.63 6.23777V6.25H12.3766C12.376 6.25 12.3755 6.25 12.375 6.25H11V9.75H12.375C13.8247 9.75 15 10.9253 15 12.375C15 13.8247 13.8247 15 12.375 15C11.0113 15 9.89045 13.9601 9.76223 12.63H9.75V12.3773L9.75 12.375V11H6.25V12.375C6.25 13.8247 5.07475 15 3.625 15C2.17525 15 1 13.8247 1 12.375C1 11.0113 2.03991 9.89045 3.37 9.76223V9.75H3.62274L3.625 9.75H5L5 6.25H3.625C2.17525 6.25 1 5.07475 1 3.625C1 2.17525 2.17525 1 3.625 1C4.98872 1 6.10955 2.03991 6.23777 3.37H6.25L6.25 5L9.75 5ZM9.75 6.25L6.25 6.25L6.25 9.75H9.75V6.25ZM3.625 11H5V12.375C5 13.1344 4.38439 13.75 3.625 13.75C2.86561 13.75 2.25 13.1344 2.25 12.375C2.25 11.6162 2.86472 11.0009 3.62336 11L3.625 11ZM11 12.3766C11.0009 13.1353 11.6162 13.75 12.375 13.75C13.1344 13.75 13.75 13.1344 13.75 12.375C13.75 11.6156 13.1344 11 12.375 11H11V12.375L11 12.3766ZM3.625 5C2.86561 5 2.25 4.38439 2.25 3.625C2.25 2.86561 2.86561 2.25 3.625 2.25C4.38439 2.25 5 2.86561 5 3.625V5H3.625Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
3
assets/icons/control.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.5 6.12488L7.64656 1.97853C7.84183 1.78328 8.1584 1.78329 8.35366 1.97854L12.5 6.12488" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 262 B |
1
assets/icons/copy.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-copy"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
||||
|
After Width: | Height: | Size: 338 B |
3
assets/icons/option.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.35606 1.005H1.62545C1.28002 1.005 1 1.28502 1 1.63044C1 1.97587 1.28002 2.25589 1.62545 2.25589L5.35606 2.25589C5.62311 2.25589 5.8607 2.42545 5.94752 2.67799L9.75029 13.7387C10.0108 14.4963 10.7235 15.005 11.5247 15.005H14.3746C14.72 15.005 15 14.725 15 14.3796C15 14.0341 14.72 13.7541 14.3746 13.7541H11.5247C11.2576 13.7541 11.02 13.5845 10.9332 13.332L7.13046 2.27128C6.86998 1.51366 6.15721 1.005 5.35606 1.005ZM14.3745 1.005H9.75125C9.40582 1.005 9.1258 1.28502 9.1258 1.63044C9.1258 1.97587 9.40582 2.25589 9.75125 2.25589L14.3745 2.25589C14.72 2.25589 15 1.97587 15 1.63044C15 1.28502 14.72 1.005 14.3745 1.005Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 792 B |
3
assets/icons/return.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.375 1.63C8.375 1.28482 8.65482 1.005 9 1.005H12.375C13.8247 1.005 15 2.18025 15 3.63V7.625C15 9.07474 13.8247 10.25 12.375 10.25H3.13388L6.07194 13.1881C6.31602 13.4321 6.31602 13.8279 6.07194 14.0719C5.82786 14.316 5.43214 14.316 5.18806 14.0719L1.18306 10.0669C0.938981 9.82286 0.938981 9.42714 1.18306 9.18306L5.18306 5.18306C5.42714 4.93898 5.82286 4.93898 6.06694 5.18306C6.31102 5.42714 6.31102 5.82286 6.06694 6.06694L3.13388 9H12.375C13.1344 9 13.75 8.38439 13.75 7.625V3.63C13.75 2.87061 13.1344 2.255 12.375 2.255H9C8.65482 2.255 8.375 1.97518 8.375 1.63Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 737 B |
3
assets/icons/shift.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.46475 7.99652L7.85304 2.15921C7.93223 2.07342 8.06777 2.07341 8.14696 2.15921L13.5352 7.99652C13.7126 8.18869 13.5763 8.5 13.3148 8.5H10.5V13.7C10.5 13.8657 10.3657 14 10.2 14H5.8C5.63431 14 5.5 13.8657 5.5 13.7V8.5H2.6852C2.42367 8.5 2.28737 8.18869 2.46475 7.99652Z" stroke="black" stroke-width="1.25"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -17,18 +17,9 @@
|
||||
"cmd-enter": "menu::SecondaryConfirm",
|
||||
"escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k u": "pane::CloseCleanItems",
|
||||
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"cmd-o": "workspace::Open",
|
||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize",
|
||||
"cmd--": "zed::DecreaseBufferFontSize",
|
||||
@@ -38,15 +29,7 @@
|
||||
"cmd-h": "zed::Hide",
|
||||
"alt-cmd-h": "zed::HideOthers",
|
||||
"cmd-m": "zed::Minimize",
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open",
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"shift-escape": "workspace::ToggleZoom"
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -284,6 +267,15 @@
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"cmd-{": "pane::ActivatePrevItem",
|
||||
"cmd-}": "pane::ActivateNextItem",
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
|
||||
"cmd-k u": "pane::CloseCleanItems",
|
||||
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||
"cmd-f": "project_search::ToggleFocus",
|
||||
"cmd-g": "search::SelectNextMatch",
|
||||
"cmd-shift-g": "search::SelectPrevMatch",
|
||||
@@ -389,6 +381,14 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
"cmd-2": ["workspace::ActivatePane", 1],
|
||||
"cmd-3": ["workspace::ActivatePane", 2],
|
||||
@@ -530,12 +530,17 @@
|
||||
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"space": "project_panel::Open",
|
||||
"backspace": "project_panel::Delete",
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel && not_editing",
|
||||
"bindings": {
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
|
||||
@@ -17,5 +17,8 @@
|
||||
"file_name": "rose-pine-dawn.json",
|
||||
"appearance": "light"
|
||||
}
|
||||
]
|
||||
],
|
||||
"syntax": {
|
||||
"function": ["entity.name"]
|
||||
}
|
||||
}
|
||||
|
||||
28
crates/activity_indicator2/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "activity_indicator2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/activity_indicator.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
auto_update = { path = "../auto_update2", package = "auto_update2" }
|
||||
editor = { path = "../editor2", package = "editor2" }
|
||||
language = { path = "../language2", package = "language2" }
|
||||
gpui = { path = "../gpui2", package = "gpui2" }
|
||||
project = { path = "../project2", package = "project2" }
|
||||
settings = { path = "../settings2", package = "settings2" }
|
||||
ui = { path = "../ui2", package = "ui2" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme2", package = "theme2" }
|
||||
workspace = { path = "../workspace2", package = "workspace2" }
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
|
||||
333
crates/activity_indicator2/src/activity_indicator.rs
Normal file
@@ -0,0 +1,333 @@
|
||||
use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||
use editor::Editor;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions, svg, AppContext, CursorStyle, Div, EventEmitter, InteractiveElement as _, Model,
|
||||
ParentElement as _, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, View,
|
||||
ViewContext, VisualContext as _,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||
use ui::h_stack;
|
||||
use util::ResultExt;
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
actions!(ShowErrorMessage);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download.svg";
|
||||
const WARNING_ICON: &str = "icons/warning.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
}
|
||||
|
||||
pub struct ActivityIndicator {
|
||||
statuses: Vec<LspStatus>,
|
||||
project: Model<Project>,
|
||||
auto_updater: Option<Model<AutoUpdater>>,
|
||||
}
|
||||
|
||||
struct LspStatus {
|
||||
name: Arc<str>,
|
||||
status: LanguageServerBinaryStatus,
|
||||
}
|
||||
|
||||
struct PendingWork<'a> {
|
||||
language_server_name: &'a str,
|
||||
progress_token: &'a str,
|
||||
progress: &'a LanguageServerProgress,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Content {
|
||||
icon: Option<&'static str>,
|
||||
message: String,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
}
|
||||
|
||||
impl ActivityIndicator {
|
||||
pub fn new(
|
||||
workspace: &mut Workspace,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> View<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let this = cx.build_view(|cx: &mut ViewContext<Self>| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((language, event)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != language.name());
|
||||
this.statuses.push(LspStatus {
|
||||
name: language.name(),
|
||||
status: event,
|
||||
});
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
if let Some(auto_updater) = auto_updater.as_ref() {
|
||||
cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
// cx.observe_active_labeled_tasks(|_, cx| cx.notify())
|
||||
// .detach();
|
||||
|
||||
Self {
|
||||
statuses: Default::default(),
|
||||
project: project.clone(),
|
||||
auto_updater,
|
||||
}
|
||||
});
|
||||
|
||||
cx.subscribe(&this, move |workspace, _, event, cx| match event {
|
||||
Event::ShowError { lsp_name, error } => {
|
||||
if let Some(buffer) = project
|
||||
.update(cx, |project, cx| project.create_buffer(error, None, cx))
|
||||
.log_err()
|
||||
{
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(0..0, format!("Language server error: {}\n\n", lsp_name))],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
workspace.add_item(
|
||||
Box::new(cx.build_view(|cx| {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), cx)
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
self.statuses.retain(|status| {
|
||||
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
error: error.clone(),
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
updater.update(cx, |updater, cx| {
|
||||
updater.dismiss_error(cx);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn pending_language_server_work<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = PendingWork<'a>> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.language_server_statuses()
|
||||
.rev()
|
||||
.filter_map(|status| {
|
||||
if status.pending_work.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut pending_work = status
|
||||
.pending_work
|
||||
.iter()
|
||||
.map(|(token, progress)| PendingWork {
|
||||
language_server_name: status.name.as_str(),
|
||||
progress_token: token.as_str(),
|
||||
progress,
|
||||
})
|
||||
.collect::<SmallVec<[_; 4]>>();
|
||||
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
|
||||
Some(pending_work)
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
language_server_name,
|
||||
progress_token,
|
||||
progress,
|
||||
}) = pending_work.next()
|
||||
{
|
||||
let mut message = language_server_name.to_string();
|
||||
|
||||
message.push_str(": ");
|
||||
if let Some(progress_message) = progress.message.as_ref() {
|
||||
message.push_str(progress_message);
|
||||
} else {
|
||||
message.push_str(progress_token);
|
||||
}
|
||||
|
||||
if let Some(percentage) = progress.percentage {
|
||||
write!(&mut message, " ({}%)", percentage).unwrap();
|
||||
}
|
||||
|
||||
let additional_work_count = pending_work.count();
|
||||
if additional_work_count > 0 {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return Content {
|
||||
icon: None,
|
||||
message,
|
||||
on_click: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Show any language server installation info.
|
||||
let mut downloading = SmallVec::<[_; 3]>::new();
|
||||
let mut checking_for_update = SmallVec::<[_; 3]>::new();
|
||||
let mut failed = SmallVec::<[_; 3]>::new();
|
||||
for status in &self.statuses {
|
||||
let name = status.name.clone();
|
||||
match status.status {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
|
||||
LanguageServerBinaryStatus::Downloading => downloading.push(name),
|
||||
LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
|
||||
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Downloading {} language server{}...",
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
on_click: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Checking for updates to {} language server{}...",
|
||||
checking_for_update.join(", "),
|
||||
if checking_for_update.len() > 1 {
|
||||
"s"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
),
|
||||
on_click: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
return Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: format!(
|
||||
"Failed to download {} language server{}. Click to show error.",
|
||||
failed.join(", "),
|
||||
if failed.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.show_error_message(&Default::default(), cx)
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
workspace::restart(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: "Auto update failed".to_string(),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
};
|
||||
}
|
||||
|
||||
// todo!(show active tasks)
|
||||
// if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
||||
// return Content {
|
||||
// icon: None,
|
||||
// message: most_recent_active_task.to_string(),
|
||||
// on_click: None,
|
||||
// };
|
||||
// }
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ActivityIndicator {}
|
||||
|
||||
impl Render for ActivityIndicator {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let content = self.content_to_render(cx);
|
||||
|
||||
let mut result = h_stack()
|
||||
.id("activity-indicator")
|
||||
.on_action(cx.listener(Self::show_error_message))
|
||||
.on_action(cx.listener(Self::dismiss_error_message));
|
||||
|
||||
if let Some(on_click) = content.on_click {
|
||||
result = result
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
on_click(this, cx);
|
||||
}))
|
||||
}
|
||||
|
||||
result
|
||||
.children(content.icon.map(|icon| svg().path(icon)))
|
||||
.child(SharedString::from(content.message))
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for ActivityIndicator {
|
||||
fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ pub enum ProviderCredential {
|
||||
NotNeeded,
|
||||
}
|
||||
|
||||
pub trait CredentialProvider {
|
||||
pub trait CredentialProvider: Send + Sync {
|
||||
fn has_credentials(&self) -> bool;
|
||||
fn retrieve_credentials(&self, cx: &mut AppContext) -> ProviderCredential;
|
||||
fn save_credentials(&self, cx: &mut AppContext, credential: ProviderCredential);
|
||||
|
||||
@@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent {
|
||||
|
||||
pub async fn stream_completion(
|
||||
credential: ProviderCredential,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
request: Box<dyn CompletionRequest>,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
let api_key = match credential {
|
||||
@@ -197,11 +197,11 @@ pub async fn stream_completion(
|
||||
pub struct OpenAICompletionProvider {
|
||||
model: OpenAILanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
executor: Arc<BackgroundExecutor>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
impl OpenAICompletionProvider {
|
||||
pub fn new(model_name: &str, executor: Arc<BackgroundExecutor>) -> Self {
|
||||
pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self {
|
||||
let model = OpenAILanguageModel::load(model_name);
|
||||
let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials));
|
||||
Self {
|
||||
|
||||
@@ -35,7 +35,7 @@ pub struct OpenAIEmbeddingProvider {
|
||||
model: OpenAILanguageModel,
|
||||
credential: Arc<RwLock<ProviderCredential>>,
|
||||
pub client: Arc<dyn HttpClient>,
|
||||
pub executor: Arc<BackgroundExecutor>,
|
||||
pub executor: BackgroundExecutor,
|
||||
rate_limit_count_rx: watch::Receiver<Option<Instant>>,
|
||||
rate_limit_count_tx: Arc<Mutex<watch::Sender<Option<Instant>>>>,
|
||||
}
|
||||
@@ -66,7 +66,7 @@ struct OpenAIEmbeddingUsage {
|
||||
}
|
||||
|
||||
impl OpenAIEmbeddingProvider {
|
||||
pub fn new(client: Arc<dyn HttpClient>, executor: Arc<BackgroundExecutor>) -> Self {
|
||||
pub fn new(client: Arc<dyn HttpClient>, executor: BackgroundExecutor) -> Self {
|
||||
let (rate_limit_count_tx, rate_limit_count_rx) = watch::channel_with(None);
|
||||
let rate_limit_count_tx = Arc::new(Mutex::new(rate_limit_count_tx));
|
||||
|
||||
|
||||
@@ -1218,6 +1218,31 @@ impl View for AssistantPanel {
|
||||
let style = &theme.assistant;
|
||||
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Text::new(
|
||||
"To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" - Having a subscription for another service like GitHub Copilot won't work.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" - You can create a api key at: platform.openai.com/api-keys",
|
||||
style.api_key_prompt.text.clone(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" ",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
"Paste your OpenAI API key and press Enter to use the assistant",
|
||||
@@ -1231,6 +1256,20 @@ impl View for AssistantPanel {
|
||||
.with_style(style.api_key_editor.container)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
" ",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new(
|
||||
"Click on the Z button in the status bar to close this panel.",
|
||||
style.api_key_prompt.text.clone(),
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.api_key_prompt.container)
|
||||
.aligned()
|
||||
|
||||
54
crates/assistant2/Cargo.toml
Normal file
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "assistant2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ai = { package = "ai2", path = "../ai2" }
|
||||
client = { package = "client2", path = "../client2" }
|
||||
collections = { path = "../collections"}
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fs = { package = "fs2", path = "../fs2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
menu = { package = "menu2", path = "../menu2" }
|
||||
multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
semantic_index = { package = "semantic_index2", path = "../semantic_index2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
|
||||
uuid.workspace = true
|
||||
log.workspace = true
|
||||
anyhow.workspace = true
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
isahc.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
regex.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tiktoken-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ai = { package = "ai2", path = "../ai2", features = ["test-support"]}
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
log.workspace = true
|
||||
rand.workspace = true
|
||||
63
crates/assistant2/README.zmd
Normal file
@@ -0,0 +1,63 @@
|
||||
> Hello, I'm Nathan Sobo.
|
||||
I'm currently your sole creator and the leader of Zed Industries.
|
||||
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
|
||||
|
||||
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
|
||||
|
||||
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
|
||||
|
||||
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
|
||||
|
||||
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
|
||||
|
||||
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
|
||||
|
||||
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
|
||||
|
||||
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
|
||||
|
||||
@nathan> #zed, can you describe the conversation UI as you currently understand it.
|
||||
|
||||
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
|
||||
|
||||
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
|
||||
|
||||
@nathan> What details around the conversation view are worth exploring?
|
||||
|
||||
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
|
||||
|
||||
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
|
||||
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
|
||||
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
|
||||
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
|
||||
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
|
||||
|
||||
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
|
||||
|
||||
@nathan> @nate, which of these would you like to explore?
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
#zed describe a hypothetical conversation UI that meets the description.
|
||||
|
||||
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
|
||||
|
||||
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
|
||||
|
||||
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
|
||||
|
||||
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
|
||||
|
||||
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
|
||||
|
||||
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
|
||||
|
||||
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
|
||||
|
||||
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
|
||||
|
||||
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.
|
||||
3
crates/assistant2/features.zmd
Normal file
@@ -0,0 +1,3 @@
|
||||
Push content to a deeper layer.
|
||||
A context can have multiple sublayers.
|
||||
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.
|
||||
126
crates/assistant2/src/assistant.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
pub mod assistant_panel;
|
||||
mod assistant_settings;
|
||||
mod codegen;
|
||||
mod prompts;
|
||||
mod streaming_diff;
|
||||
|
||||
use ai::providers::open_ai::Role;
|
||||
use anyhow::Result;
|
||||
pub use assistant_panel::AssistantPanel;
|
||||
use assistant_settings::OpenAIModel;
|
||||
use chrono::{DateTime, Local};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{actions, AppContext, SharedString};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc};
|
||||
use util::paths::CONVERSATIONS_DIR;
|
||||
|
||||
actions!(
|
||||
NewConversation,
|
||||
Assist,
|
||||
Split,
|
||||
CycleMessageRole,
|
||||
QuoteSelection,
|
||||
ToggleFocus,
|
||||
ResetKey,
|
||||
InlineAssist,
|
||||
ToggleIncludeConversation,
|
||||
ToggleRetrieveContext,
|
||||
);
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
sent_at: DateTime<Local>,
|
||||
status: MessageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum MessageStatus {
|
||||
Pending,
|
||||
Done,
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedMessage {
|
||||
id: MessageId,
|
||||
start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversation {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
model: OpenAIModel,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
const VERSION: &'static str = "0.1.0";
|
||||
}
|
||||
|
||||
struct SavedConversationMetadata {
|
||||
title: String,
|
||||
path: PathBuf,
|
||||
mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
impl SavedConversationMetadata {
|
||||
pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pattern = r" - \d+.zed.json$";
|
||||
let re = Regex::new(pattern).unwrap();
|
||||
|
||||
let metadata = fs.metadata(&path).await?;
|
||||
if let Some((file_name, metadata)) = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
let title = re.replace(file_name, "");
|
||||
conversations.push(Self {
|
||||
title: title.into_owned(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
});
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
|
||||
Ok(conversations)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
assistant_panel::init(cx);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::init();
|
||||
}
|
||||
}
|
||||
3480
crates/assistant2/src/assistant_panel.rs
Normal file
80
crates/assistant2/src/assistant_settings.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub enum OpenAIModel {
|
||||
#[serde(rename = "gpt-3.5-turbo-0613")]
|
||||
ThreePointFiveTurbo,
|
||||
#[serde(rename = "gpt-4-0613")]
|
||||
Four,
|
||||
#[serde(rename = "gpt-4-1106-preview")]
|
||||
FourTurbo,
|
||||
}
|
||||
|
||||
impl OpenAIModel {
|
||||
pub fn full_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613",
|
||||
OpenAIModel::Four => "gpt-4-0613",
|
||||
OpenAIModel::FourTurbo => "gpt-4-1106-preview",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn short_name(&self) -> &'static str {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
OpenAIModel::Four => "gpt-4",
|
||||
OpenAIModel::FourTurbo => "gpt-4-turbo",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(&self) -> Self {
|
||||
match self {
|
||||
OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four,
|
||||
OpenAIModel::Four => OpenAIModel::FourTurbo,
|
||||
OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct AssistantSettings {
|
||||
pub button: bool,
|
||||
pub dock: AssistantDockPosition,
|
||||
pub default_width: f32,
|
||||
pub default_height: f32,
|
||||
pub default_open_ai_model: OpenAIModel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct AssistantSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<AssistantDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
pub default_height: Option<f32>,
|
||||
pub default_open_ai_model: Option<OpenAIModel>,
|
||||
}
|
||||
|
||||
impl Settings for AssistantSettings {
|
||||
const KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
type FileContent = AssistantSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &mut gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
688
crates/assistant2/src/codegen.rs
Normal file
@@ -0,0 +1,688 @@
|
||||
use crate::streaming_diff::{Hunk, StreamingDiff};
|
||||
use ai::completion::{CompletionProvider, CompletionRequest};
|
||||
use anyhow::Result;
|
||||
use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use futures::{channel::mpsc, SinkExt, Stream, StreamExt};
|
||||
use gpui::{EventEmitter, Model, ModelContext, Task};
|
||||
use language::{Rope, TransactionId};
|
||||
use multi_buffer;
|
||||
use std::{cmp, future, ops::Range, sync::Arc};
|
||||
|
||||
pub enum Event {
|
||||
Finished,
|
||||
Undone,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum CodegenKind {
|
||||
Transform { range: Range<Anchor> },
|
||||
Generate { position: Anchor },
|
||||
}
|
||||
|
||||
pub struct Codegen {
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
buffer: Model<MultiBuffer>,
|
||||
snapshot: MultiBufferSnapshot,
|
||||
kind: CodegenKind,
|
||||
last_equal_ranges: Vec<Range<Anchor>>,
|
||||
transaction_id: Option<TransactionId>,
|
||||
error: Option<anyhow::Error>,
|
||||
generation: Task<()>,
|
||||
idle: bool,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Codegen {}
|
||||
|
||||
impl Codegen {
|
||||
pub fn new(
|
||||
buffer: Model<MultiBuffer>,
|
||||
kind: CodegenKind,
|
||||
provider: Arc<dyn CompletionProvider>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
Self {
|
||||
provider,
|
||||
buffer: buffer.clone(),
|
||||
snapshot,
|
||||
kind,
|
||||
last_equal_ranges: Default::default(),
|
||||
transaction_id: Default::default(),
|
||||
error: Default::default(),
|
||||
idle: true,
|
||||
generation: Task::ready(()),
|
||||
_subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_buffer_event(
|
||||
&mut self,
|
||||
_buffer: Model<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::TransactionUndone { transaction_id } = event {
|
||||
if self.transaction_id == Some(*transaction_id) {
|
||||
self.transaction_id = None;
|
||||
self.generation = Task::ready(());
|
||||
cx.emit(Event::Undone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range(&self) -> Range<Anchor> {
|
||||
match &self.kind {
|
||||
CodegenKind::Transform { range } => range.clone(),
|
||||
CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &CodegenKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn last_equal_ranges(&self) -> &[Range<Anchor>] {
|
||||
&self.last_equal_ranges
|
||||
}
|
||||
|
||||
pub fn idle(&self) -> bool {
|
||||
self.idle
|
||||
}
|
||||
|
||||
pub fn error(&self) -> Option<&anyhow::Error> {
|
||||
self.error.as_ref()
|
||||
}
|
||||
|
||||
pub fn start(&mut self, prompt: Box<dyn CompletionRequest>, cx: &mut ModelContext<Self>) {
|
||||
let range = self.range();
|
||||
let snapshot = self.snapshot.clone();
|
||||
let selected_text = snapshot
|
||||
.text_for_range(range.start..range.end)
|
||||
.collect::<Rope>();
|
||||
|
||||
let selection_start = range.start.to_point(&snapshot);
|
||||
let suggested_line_indent = snapshot
|
||||
.suggested_indents(selection_start.row..selection_start.row + 1, cx)
|
||||
.into_values()
|
||||
.next()
|
||||
.unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row));
|
||||
|
||||
let response = self.provider.complete(prompt);
|
||||
self.generation = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let generate = async {
|
||||
let mut edit_start = range.start.to_offset(&snapshot);
|
||||
|
||||
let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
|
||||
let diff = cx.background_executor().spawn(async move {
|
||||
let chunks = strip_invalid_spans_from_codeblock(response.await?);
|
||||
futures::pin_mut!(chunks);
|
||||
let mut diff = StreamingDiff::new(selected_text.to_string());
|
||||
|
||||
let mut new_text = String::new();
|
||||
let mut base_indent = None;
|
||||
let mut line_indent = None;
|
||||
let mut first_line = true;
|
||||
|
||||
while let Some(chunk) = chunks.next().await {
|
||||
let chunk = chunk?;
|
||||
|
||||
let mut lines = chunk.split('\n').peekable();
|
||||
while let Some(line) = lines.next() {
|
||||
new_text.push_str(line);
|
||||
if line_indent.is_none() {
|
||||
if let Some(non_whitespace_ch_ix) =
|
||||
new_text.find(|ch: char| !ch.is_whitespace())
|
||||
{
|
||||
line_indent = Some(non_whitespace_ch_ix);
|
||||
base_indent = base_indent.or(line_indent);
|
||||
|
||||
let line_indent = line_indent.unwrap();
|
||||
let base_indent = base_indent.unwrap();
|
||||
let indent_delta = line_indent as i32 - base_indent as i32;
|
||||
let mut corrected_indent_len = cmp::max(
|
||||
0,
|
||||
suggested_line_indent.len as i32 + indent_delta,
|
||||
)
|
||||
as usize;
|
||||
if first_line {
|
||||
corrected_indent_len = corrected_indent_len
|
||||
.saturating_sub(selection_start.column as usize);
|
||||
}
|
||||
|
||||
let indent_char = suggested_line_indent.char();
|
||||
let mut indent_buffer = [0; 4];
|
||||
let indent_str =
|
||||
indent_char.encode_utf8(&mut indent_buffer);
|
||||
new_text.replace_range(
|
||||
..line_indent,
|
||||
&indent_str.repeat(corrected_indent_len),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if line_indent.is_some() {
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
new_text.clear();
|
||||
}
|
||||
|
||||
if lines.peek().is_some() {
|
||||
hunks_tx.send(diff.push_new("\n")).await?;
|
||||
line_indent = None;
|
||||
first_line = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
hunks_tx.send(diff.push_new(&new_text)).await?;
|
||||
hunks_tx.send(diff.finish()).await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
while let Some(hunks) = hunks_rx.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
|
||||
let transaction = this.buffer.update(cx, |buffer, cx| {
|
||||
// Avoid grouping assistant edits with user edits.
|
||||
buffer.finalize_last_transaction(cx);
|
||||
|
||||
buffer.start_transaction(cx);
|
||||
buffer.edit(
|
||||
hunks.into_iter().filter_map(|hunk| match hunk {
|
||||
Hunk::Insert { text } => {
|
||||
let edit_start = snapshot.anchor_after(edit_start);
|
||||
Some((edit_start..edit_start, text))
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
Some((edit_range, String::new()))
|
||||
}
|
||||
Hunk::Keep { len } => {
|
||||
let edit_end = edit_start + len;
|
||||
let edit_range = snapshot.anchor_after(edit_start)
|
||||
..snapshot.anchor_before(edit_end);
|
||||
edit_start = edit_end;
|
||||
this.last_equal_ranges.push(edit_range);
|
||||
None
|
||||
}
|
||||
}),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
|
||||
buffer.end_transaction(cx)
|
||||
});
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
if let Some(first_transaction) = this.transaction_id {
|
||||
// Group all assistant edits into the first transaction.
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.merge_transactions(
|
||||
transaction,
|
||||
first_transaction,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
} else {
|
||||
this.transaction_id = Some(transaction);
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction(cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
diff.await?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
let result = generate.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_equal_ranges.clear();
|
||||
this.idle = true;
|
||||
if let Err(error) = result {
|
||||
this.error = Some(error);
|
||||
}
|
||||
cx.emit(Event::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
});
|
||||
self.error.take();
|
||||
self.idle = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if let Some(transaction_id) = self.transaction_id {
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_invalid_spans_from_codeblock(
|
||||
stream: impl Stream<Item = Result<String>>,
|
||||
) -> impl Stream<Item = Result<String>> {
|
||||
let mut first_line = true;
|
||||
let mut buffer = String::new();
|
||||
let mut starts_with_markdown_codeblock = false;
|
||||
let mut includes_start_or_end_span = false;
|
||||
stream.filter_map(move |chunk| {
|
||||
let chunk = match chunk {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => return future::ready(Some(Err(err))),
|
||||
};
|
||||
buffer.push_str(&chunk);
|
||||
|
||||
if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") {
|
||||
includes_start_or_end_span = true;
|
||||
|
||||
buffer = buffer
|
||||
.strip_prefix("<|S|>")
|
||||
.or_else(|| buffer.strip_prefix("<|S|"))
|
||||
.unwrap_or(&buffer)
|
||||
.to_string();
|
||||
} else if buffer.ends_with("|E|>") {
|
||||
includes_start_or_end_span = true;
|
||||
} else if buffer.starts_with("<|")
|
||||
|| buffer.starts_with("<|S")
|
||||
|| buffer.starts_with("<|S|")
|
||||
|| buffer.ends_with("|")
|
||||
|| buffer.ends_with("|E")
|
||||
|| buffer.ends_with("|E|")
|
||||
{
|
||||
return future::ready(None);
|
||||
}
|
||||
|
||||
if first_line {
|
||||
if buffer == "" || buffer == "`" || buffer == "``" {
|
||||
return future::ready(None);
|
||||
} else if buffer.starts_with("```") {
|
||||
starts_with_markdown_codeblock = true;
|
||||
if let Some(newline_ix) = buffer.find('\n') {
|
||||
buffer.replace_range(..newline_ix + 1, "");
|
||||
first_line = false;
|
||||
} else {
|
||||
return future::ready(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut text = buffer.to_string();
|
||||
if starts_with_markdown_codeblock {
|
||||
text = text
|
||||
.strip_suffix("\n```\n")
|
||||
.or_else(|| text.strip_suffix("\n```"))
|
||||
.or_else(|| text.strip_suffix("\n``"))
|
||||
.or_else(|| text.strip_suffix("\n`"))
|
||||
.or_else(|| text.strip_suffix('\n'))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
}
|
||||
|
||||
if includes_start_or_end_span {
|
||||
text = text
|
||||
.strip_suffix("|E|>")
|
||||
.or_else(|| text.strip_suffix("E|>"))
|
||||
.or_else(|| text.strip_prefix("|>"))
|
||||
.or_else(|| text.strip_prefix(">"))
|
||||
.unwrap_or(&text)
|
||||
.to_string();
|
||||
};
|
||||
|
||||
if text.contains('\n') {
|
||||
first_line = false;
|
||||
}
|
||||
|
||||
let remainder = buffer.split_off(text.len());
|
||||
let result = if buffer.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Ok(buffer.clone()))
|
||||
};
|
||||
|
||||
buffer = remainder;
|
||||
future::ready(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use ai::test::FakeCompletionProvider;
|
||||
use futures::stream::{self};
|
||||
use gpui::{Context, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct DummyCompletionRequest {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl CompletionRequest for DummyCompletionRequest {
|
||||
fn data(&self) -> serde_json::Result<String> {
|
||||
serde_json::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
let x = 0;
|
||||
for _ in 0..10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let range = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Transform { range },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
" let mut x = 0;\n",
|
||||
" while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
" }",
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("CHUNK: {:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_past_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = indoc! {"
|
||||
fn main() {
|
||||
le
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 6))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"t mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_autoindent_when_generating_before_indentation(
|
||||
cx: &mut TestAppContext,
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx.set_global(cx.update(SettingsStore::test));
|
||||
cx.update(language_settings::init);
|
||||
|
||||
let text = concat!(
|
||||
"fn main() {\n",
|
||||
" \n",
|
||||
"}\n" //
|
||||
);
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let position = buffer.read_with(cx, |buffer, cx| {
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
snapshot.anchor_before(Point::new(1, 2))
|
||||
});
|
||||
let provider = Arc::new(FakeCompletionProvider::new());
|
||||
let codegen = cx.build_model(|cx| {
|
||||
Codegen::new(
|
||||
buffer.clone(),
|
||||
CodegenKind::Generate { position },
|
||||
provider.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let request = Box::new(DummyCompletionRequest {
|
||||
name: "test".to_string(),
|
||||
});
|
||||
codegen.update(cx, |codegen, cx| codegen.start(request, cx));
|
||||
|
||||
let mut new_text = concat!(
|
||||
"let mut x = 0;\n",
|
||||
"while x < 10 {\n",
|
||||
" x += 1;\n",
|
||||
"}", //
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
println!("{:?}", &chunk);
|
||||
provider.send_completion(chunk);
|
||||
new_text = suffix;
|
||||
cx.background_executor.run_until_parked();
|
||||
}
|
||||
provider.finish_completion();
|
||||
cx.background_executor.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
let mut x = 0;
|
||||
while x < 10 {
|
||||
x += 1;
|
||||
}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_strip_invalid_spans_from_codeblock() {
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum dolor"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks(
|
||||
"```html\n```js\nLorem ipsum dolor\n```\n```",
|
||||
2
|
||||
))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"```js\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"``\nLorem ipsum dolor\n```"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2))
|
||||
.map(|chunk| chunk.unwrap())
|
||||
.collect::<String>()
|
||||
.await,
|
||||
"Lorem ipsum"
|
||||
);
|
||||
fn chunks(text: &str, size: usize) -> impl Stream<Item = Result<String>> {
|
||||
stream::iter(
|
||||
text.chars()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(size)
|
||||
.map(|chunk| Ok(chunk.iter().collect::<String>()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
389
crates/assistant2/src/prompts.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
use ai::models::LanguageModel;
|
||||
use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate};
|
||||
use ai::prompts::file_context::FileContext;
|
||||
use ai::prompts::generate::GenerateInlineContent;
|
||||
use ai::prompts::preamble::EngineerPreamble;
|
||||
use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext};
|
||||
use ai::providers::open_ai::OpenAILanguageModel;
|
||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||
use std::cmp::{self, Reverse};
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||
#[derive(Debug)]
|
||||
struct Match {
|
||||
collapse: Range<usize>,
|
||||
keep: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
let selected_range = selected_range.to_offset(buffer);
|
||||
let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| {
|
||||
Some(&grammar.embedding_config.as_ref()?.query)
|
||||
});
|
||||
let configs = ts_matches
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|g| g.embedding_config.as_ref().unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
let mut matches = Vec::new();
|
||||
while let Some(mat) = ts_matches.peek() {
|
||||
let config = &configs[mat.grammar_index];
|
||||
if let Some(collapse) = mat.captures.iter().find_map(|cap| {
|
||||
if Some(cap.index) == config.collapse_capture_ix {
|
||||
Some(cap.node.byte_range())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let mut keep = Vec::new();
|
||||
for capture in mat.captures.iter() {
|
||||
if Some(capture.index) == config.keep_capture_ix {
|
||||
keep.push(capture.node.byte_range());
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ts_matches.advance();
|
||||
matches.push(Match { collapse, keep });
|
||||
} else {
|
||||
ts_matches.advance();
|
||||
}
|
||||
}
|
||||
matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end)));
|
||||
let mut matches = matches.into_iter().peekable();
|
||||
|
||||
let mut summary = String::new();
|
||||
let mut offset = 0;
|
||||
let mut flushed_selection = false;
|
||||
while let Some(mat) = matches.next() {
|
||||
// Keep extending the collapsed range if the next match surrounds
|
||||
// the current one.
|
||||
while let Some(next_mat) = matches.peek() {
|
||||
if mat.collapse.start <= next_mat.collapse.start
|
||||
&& mat.collapse.end >= next_mat.collapse.end
|
||||
{
|
||||
matches.next().unwrap();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if offset > mat.collapse.start {
|
||||
// Skip collapsed nodes that have already been summarized.
|
||||
offset = cmp::max(offset, mat.collapse.end);
|
||||
continue;
|
||||
}
|
||||
|
||||
if offset <= selected_range.start && selected_range.start <= mat.collapse.end {
|
||||
if !flushed_selection {
|
||||
// The collapsed node ends after the selection starts, so we'll flush the selection first.
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
flushed_selection = true;
|
||||
}
|
||||
|
||||
// If the selection intersects the collapsed node, we won't collapse it.
|
||||
if selected_range.end >= mat.collapse.start {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..mat.collapse.start));
|
||||
for keep in mat.keep {
|
||||
summary.extend(buffer.text_for_range(keep));
|
||||
}
|
||||
offset = mat.collapse.end;
|
||||
}
|
||||
|
||||
// Flush selection if we haven't already done so.
|
||||
if !flushed_selection && offset <= selected_range.start {
|
||||
summary.extend(buffer.text_for_range(offset..selected_range.start));
|
||||
summary.push_str("<|S|");
|
||||
if selected_range.end == selected_range.start {
|
||||
summary.push_str(">");
|
||||
} else {
|
||||
summary.extend(buffer.text_for_range(selected_range.clone()));
|
||||
summary.push_str("|E|>");
|
||||
}
|
||||
offset = selected_range.end;
|
||||
}
|
||||
|
||||
summary.extend(buffer.text_for_range(offset..buffer.len()));
|
||||
summary
|
||||
}
|
||||
|
||||
pub fn generate_content_prompt(
|
||||
user_prompt: String,
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
search_results: Vec<PromptCodeSnippet>,
|
||||
model: &str,
|
||||
project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
// Using new Prompt Templates
|
||||
let openai_model: Arc<dyn LanguageModel> = Arc::new(OpenAILanguageModel::load(model));
|
||||
let lang_name = if let Some(language_name) = language_name {
|
||||
Some(language_name.to_string())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let args = PromptArguments {
|
||||
model: openai_model,
|
||||
language_name: lang_name.clone(),
|
||||
project_name,
|
||||
snippets: search_results.clone(),
|
||||
reserved_tokens: 1000,
|
||||
buffer: Some(buffer),
|
||||
selected_range: Some(range),
|
||||
user_prompt: Some(user_prompt.clone()),
|
||||
};
|
||||
|
||||
let templates: Vec<(PromptPriority, Box<dyn PromptTemplate>)> = vec![
|
||||
(PromptPriority::Mandatory, Box::new(EngineerPreamble {})),
|
||||
(
|
||||
PromptPriority::Ordered { order: 1 },
|
||||
Box::new(RepositoryContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Ordered { order: 0 },
|
||||
Box::new(FileContext {}),
|
||||
),
|
||||
(
|
||||
PromptPriority::Mandatory,
|
||||
Box::new(GenerateInlineContent {}),
|
||||
),
|
||||
];
|
||||
let chain = PromptChain::new(args, templates);
|
||||
let (prompt, _) = chain.generate(true)?;
|
||||
|
||||
anyhow::Ok(prompt)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{AppContext, Context};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point};
|
||||
use settings::SettingsStore;
|
||||
|
||||
pub(crate) fn rust_lang() -> Language {
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
)
|
||||
.with_embedding_query(
|
||||
r#"
|
||||
(
|
||||
[(line_comment) (attribute_item)]* @context
|
||||
.
|
||||
[
|
||||
(struct_item
|
||||
name: (_) @name)
|
||||
|
||||
(enum_item
|
||||
name: (_) @name)
|
||||
|
||||
(impl_item
|
||||
trait: (_)? @name
|
||||
"for"? @name
|
||||
type: (_) @name)
|
||||
|
||||
(trait_item
|
||||
name: (_) @name)
|
||||
|
||||
(function_item
|
||||
name: (_) @name
|
||||
body: (block
|
||||
"{" @keep
|
||||
"}" @keep) @collapse)
|
||||
|
||||
(macro_definition
|
||||
name: (_) @name)
|
||||
] @item
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_outline_for_prompt(cx: &mut AppContext) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language_settings::init(cx);
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
self.a
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
let buffer =
|
||||
cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
<|S|>a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let <|S|a |E|>= 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
<|S|>
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)),
|
||||
indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
<|S|>"}
|
||||
);
|
||||
|
||||
// Ensure nested functions get collapsed properly.
|
||||
let text = indoc! {"
|
||||
struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {
|
||||
let a = 1;
|
||||
let b = 2;
|
||||
Self { a, b }
|
||||
}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {
|
||||
let a = 30;
|
||||
fn nested() -> usize {
|
||||
3
|
||||
}
|
||||
self.a + nested()
|
||||
}
|
||||
|
||||
pub fn b(&self) -> usize {
|
||||
self.b
|
||||
}
|
||||
}
|
||||
"};
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
assert_eq!(
|
||||
summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)),
|
||||
indoc! {"
|
||||
<|S|>struct X {
|
||||
a: usize,
|
||||
b: usize,
|
||||
}
|
||||
|
||||
impl X {
|
||||
|
||||
fn new() -> Self {}
|
||||
|
||||
pub fn a(&self, param: bool) -> usize {}
|
||||
|
||||
pub fn b(&self) -> usize {}
|
||||
}
|
||||
"}
|
||||
);
|
||||
}
|
||||
}
|
||||
293
crates/assistant2/src/streaming_diff.rs
Normal file
@@ -0,0 +1,293 @@
|
||||
use collections::HashMap;
|
||||
use ordered_float::OrderedFloat;
|
||||
use std::{
|
||||
cmp,
|
||||
fmt::{self, Debug},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
struct Matrix {
|
||||
cells: Vec<f64>,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
}
|
||||
|
||||
impl Matrix {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
cells: Vec::new(),
|
||||
rows: 0,
|
||||
cols: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, rows: usize, cols: usize) {
|
||||
self.cells.resize(rows * cols, 0.);
|
||||
self.rows = rows;
|
||||
self.cols = cols;
|
||||
}
|
||||
|
||||
fn get(&self, row: usize, col: usize) -> f64 {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
self.cells[col * self.rows + row]
|
||||
}
|
||||
|
||||
fn set(&mut self, row: usize, col: usize, value: f64) {
|
||||
if row >= self.rows {
|
||||
panic!("row out of bounds")
|
||||
}
|
||||
|
||||
if col >= self.cols {
|
||||
panic!("col out of bounds")
|
||||
}
|
||||
|
||||
self.cells[col * self.rows + row] = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Matrix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f)?;
|
||||
for i in 0..self.rows {
|
||||
for j in 0..self.cols {
|
||||
write!(f, "{:5}", self.get(i, j))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Hunk {
|
||||
Insert { text: String },
|
||||
Remove { len: usize },
|
||||
Keep { len: usize },
|
||||
}
|
||||
|
||||
pub struct StreamingDiff {
|
||||
old: Vec<char>,
|
||||
new: Vec<char>,
|
||||
scores: Matrix,
|
||||
old_text_ix: usize,
|
||||
new_text_ix: usize,
|
||||
equal_runs: HashMap<(usize, usize), u32>,
|
||||
}
|
||||
|
||||
impl StreamingDiff {
|
||||
const INSERTION_SCORE: f64 = -1.;
|
||||
const DELETION_SCORE: f64 = -20.;
|
||||
const EQUALITY_BASE: f64 = 1.8;
|
||||
const MAX_EQUALITY_EXPONENT: i32 = 16;
|
||||
|
||||
pub fn new(old: String) -> Self {
|
||||
let old = old.chars().collect::<Vec<_>>();
|
||||
let mut scores = Matrix::new();
|
||||
scores.resize(old.len() + 1, 1);
|
||||
for i in 0..=old.len() {
|
||||
scores.set(i, 0, i as f64 * Self::DELETION_SCORE);
|
||||
}
|
||||
Self {
|
||||
old,
|
||||
new: Vec::new(),
|
||||
scores,
|
||||
old_text_ix: 0,
|
||||
new_text_ix: 0,
|
||||
equal_runs: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_new(&mut self, text: &str) -> Vec<Hunk> {
|
||||
self.new.extend(text.chars());
|
||||
self.scores.resize(self.old.len() + 1, self.new.len() + 1);
|
||||
|
||||
for j in self.new_text_ix + 1..=self.new.len() {
|
||||
self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE);
|
||||
for i in 1..=self.old.len() {
|
||||
let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE;
|
||||
let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE;
|
||||
let equality_score = if self.old[i - 1] == self.new[j - 1] {
|
||||
let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0);
|
||||
equal_run += 1;
|
||||
self.equal_runs.insert((i, j), equal_run);
|
||||
|
||||
let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT);
|
||||
self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent)
|
||||
} else {
|
||||
f64::NEG_INFINITY
|
||||
};
|
||||
|
||||
let score = insertion_score.max(deletion_score).max(equality_score);
|
||||
self.scores.set(i, j, score);
|
||||
}
|
||||
}
|
||||
|
||||
let mut max_score = f64::NEG_INFINITY;
|
||||
let mut next_old_text_ix = self.old_text_ix;
|
||||
let next_new_text_ix = self.new.len();
|
||||
for i in self.old_text_ix..=self.old.len() {
|
||||
let score = self.scores.get(i, next_new_text_ix);
|
||||
if score > max_score {
|
||||
max_score = score;
|
||||
next_old_text_ix = i;
|
||||
}
|
||||
}
|
||||
|
||||
let hunks = self.backtrack(next_old_text_ix, next_new_text_ix);
|
||||
self.old_text_ix = next_old_text_ix;
|
||||
self.new_text_ix = next_new_text_ix;
|
||||
hunks
|
||||
}
|
||||
|
||||
fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec<Hunk> {
|
||||
let mut pending_insert: Option<Range<usize>> = None;
|
||||
let mut hunks = Vec::new();
|
||||
let mut i = old_text_ix;
|
||||
let mut j = new_text_ix;
|
||||
while (i, j) != (self.old_text_ix, self.new_text_ix) {
|
||||
let insertion_score = if j > self.new_text_ix {
|
||||
Some((i, j - 1))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let deletion_score = if i > self.old_text_ix {
|
||||
Some((i - 1, j))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let equality_score = if i > self.old_text_ix && j > self.new_text_ix {
|
||||
if self.old[i - 1] == self.new[j - 1] {
|
||||
Some((i - 1, j - 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score]
|
||||
.iter()
|
||||
.max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j))))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
if prev_i == i && prev_j == j - 1 {
|
||||
if let Some(pending_insert) = pending_insert.as_mut() {
|
||||
pending_insert.start = prev_j;
|
||||
} else {
|
||||
pending_insert = Some(prev_j..j);
|
||||
}
|
||||
} else {
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
let char_len = self.old[i - 1].len_utf8();
|
||||
if prev_i == i - 1 && prev_j == j {
|
||||
if let Some(Hunk::Remove { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Remove { len: char_len })
|
||||
}
|
||||
} else {
|
||||
if let Some(Hunk::Keep { len }) = hunks.last_mut() {
|
||||
*len += char_len;
|
||||
} else {
|
||||
hunks.push(Hunk::Keep { len: char_len })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
i = prev_i;
|
||||
j = prev_j;
|
||||
}
|
||||
|
||||
if let Some(range) = pending_insert.take() {
|
||||
hunks.push(Hunk::Insert {
|
||||
text: self.new[range].iter().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
hunks.reverse();
|
||||
hunks
|
||||
}
|
||||
|
||||
pub fn finish(self) -> Vec<Hunk> {
|
||||
self.backtrack(self.old.len(), self.new.len())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_diffs(mut rng: StdRng) {
|
||||
let old_text_len = env::var("OLD_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
let new_text_len = env::var("NEW_TEXT_LEN")
|
||||
.map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let old = util::RandomCharIter::new(&mut rng)
|
||||
.take(old_text_len)
|
||||
.collect::<String>();
|
||||
log::info!("old text: {:?}", old);
|
||||
|
||||
let mut diff = StreamingDiff::new(old.clone());
|
||||
let mut hunks = Vec::new();
|
||||
let mut new_len = 0;
|
||||
let mut new = String::new();
|
||||
while new_len < new_text_len {
|
||||
let new_chunk_len = rng.gen_range(1..=new_text_len - new_len);
|
||||
let new_chunk = util::RandomCharIter::new(&mut rng)
|
||||
.take(new_len)
|
||||
.collect::<String>();
|
||||
log::info!("new chunk: {:?}", new_chunk);
|
||||
new_len += new_chunk_len;
|
||||
new.push_str(&new_chunk);
|
||||
let new_hunks = diff.push_new(&new_chunk);
|
||||
log::info!("hunks: {:?}", new_hunks);
|
||||
hunks.extend(new_hunks);
|
||||
}
|
||||
let final_hunks = diff.finish();
|
||||
log::info!("final hunks: {:?}", final_hunks);
|
||||
hunks.extend(final_hunks);
|
||||
|
||||
log::info!("new text: {:?}", new);
|
||||
let mut old_ix = 0;
|
||||
let mut new_ix = 0;
|
||||
let mut patched = String::new();
|
||||
for hunk in hunks {
|
||||
match hunk {
|
||||
Hunk::Keep { len } => {
|
||||
assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]);
|
||||
patched.push_str(&old[old_ix..old_ix + len]);
|
||||
old_ix += len;
|
||||
new_ix += len;
|
||||
}
|
||||
Hunk::Remove { len } => {
|
||||
old_ix += len;
|
||||
}
|
||||
Hunk::Insert { text } => {
|
||||
assert_eq!(text, &new[new_ix..new_ix + text.len()]);
|
||||
patched.push_str(&text);
|
||||
new_ix += text.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(patched, new);
|
||||
}
|
||||
}
|
||||
@@ -84,20 +84,17 @@ impl Settings for AutoUpdateSetting {
|
||||
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
|
||||
AutoUpdateSetting::register(cx);
|
||||
|
||||
cx.observe_new_views(|wokrspace: &mut Workspace, _cx| {
|
||||
wokrspace
|
||||
.register_action(|_, action: &Check, cx| check(action, cx))
|
||||
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
|
||||
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
|
||||
cx.spawn(|_, _cx| async move {
|
||||
prompt.await.ok();
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||
workspace.register_action(|_, action: &Check, cx| check(action, cx));
|
||||
|
||||
// @nate - code to trigger update notification on launch
|
||||
// workspace.show_notification(0, _cx, |cx| {
|
||||
// cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
|
||||
// });
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Some(version) = *ZED_APP_VERSION {
|
||||
if let Some(version) = ZED_APP_VERSION.or_else(|| cx.app_metadata().app_version) {
|
||||
let auto_updater = cx.build_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url);
|
||||
|
||||
@@ -125,13 +122,19 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut AppContext) {
|
||||
pub fn check(_: &Check, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
} else {
|
||||
drop(cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
"Auto-updates disabled for non-bundled app.",
|
||||
&["Ok"],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
if let Some(auto_updater) = AutoUpdater::get(cx) {
|
||||
let auto_updater = auto_updater.read(cx);
|
||||
let server_url = &auto_updater.server_url;
|
||||
|
||||
@@ -1,87 +1,56 @@
|
||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext};
|
||||
use menu::Cancel;
|
||||
use workspace::notifications::NotificationEvent;
|
||||
use gpui::{
|
||||
div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
|
||||
SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
|
||||
};
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
|
||||
|
||||
pub struct UpdateNotification {
|
||||
_version: SemanticVersion,
|
||||
version: SemanticVersion,
|
||||
}
|
||||
|
||||
impl EventEmitter<NotificationEvent> for UpdateNotification {}
|
||||
impl EventEmitter<DismissEvent> for UpdateNotification {}
|
||||
|
||||
impl Render for UpdateNotification {
|
||||
type Output = Div;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Output {
|
||||
div().child("Updated zed!")
|
||||
// let theme = theme::current(cx).clone();
|
||||
// let theme = &theme.update_notification;
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
// let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
// MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Text::new(
|
||||
// format!("Updated to {app_name} {}", self.version),
|
||||
// theme.message.text.clone(),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(theme.message.container)
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .left()
|
||||
// .flex(1., true),
|
||||
// )
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
|
||||
// let style = theme.dismiss_button.style_for(state);
|
||||
// Svg::new("icons/x.svg")
|
||||
// .with_color(style.color)
|
||||
// .constrained()
|
||||
// .with_width(style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_width(style.button_width)
|
||||
// .with_height(style.button_width)
|
||||
// })
|
||||
// .with_padding(Padding::uniform(5.))
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.dismiss(&Default::default(), cx)
|
||||
// })
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_height(cx.font_cache().line_height(theme.message.text.font_size))
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .flex_float(),
|
||||
// ),
|
||||
// )
|
||||
// .with_child({
|
||||
// let style = theme.action_message.style_for(state);
|
||||
// Text::new("View the release notes", style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// })
|
||||
// .contained()
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .on_click(MouseButton::Left, |_, _, cx| {
|
||||
// crate::view_release_notes(&Default::default(), cx)
|
||||
// })
|
||||
// .into_any_named("update notification")
|
||||
v_stack()
|
||||
.elevation_3(cx)
|
||||
.p_4()
|
||||
.child(
|
||||
h_stack()
|
||||
.justify_between()
|
||||
.child(Label::new(format!(
|
||||
"Updated to {app_name} {}",
|
||||
self.version
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.id("cancel")
|
||||
.child(IconElement::new(Icon::Close))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("notes")
|
||||
.child(Label::new("View the release notes"))
|
||||
.cursor_pointer()
|
||||
.on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateNotification {
|
||||
pub fn new(version: SemanticVersion) -> Self {
|
||||
Self { _version: version }
|
||||
Self { version }
|
||||
}
|
||||
|
||||
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(NotificationEvent::Dismiss);
|
||||
pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
28
crates/breadcrumbs2/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "breadcrumbs2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/breadcrumbs.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
collections = { path = "../collections" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
search = { package = "search2", path = "../search2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
outline = { package = "outline2", path = "../outline2" }
|
||||
itertools = "0.10"
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
140
crates/breadcrumbs2/src/breadcrumbs.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use gpui::{
|
||||
Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
pub enum Event {
|
||||
UpdateLocation,
|
||||
}
|
||||
|
||||
pub struct Breadcrumbs {
|
||||
pane_focused: bool,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
subscription: Option<Subscription>,
|
||||
workspace: WeakView<Workspace>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
pane_focused: false,
|
||||
active_item: Default::default(),
|
||||
subscription: Default::default(),
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Breadcrumbs {}
|
||||
impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
|
||||
|
||||
impl Render for Breadcrumbs {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let element = h_stack().text_ui();
|
||||
|
||||
let Some(active_item) = &self
|
||||
.active_item
|
||||
.as_ref()
|
||||
.filter(|item| item.downcast::<editor::Editor>().is_some())
|
||||
else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
|
||||
return element;
|
||||
};
|
||||
|
||||
let highlighted_segments = segments.into_iter().map(|segment| {
|
||||
StyledText::new(segment.text)
|
||||
.with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default())
|
||||
.into_any()
|
||||
});
|
||||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||
Label::new("›").into_any_element()
|
||||
});
|
||||
|
||||
element.child(
|
||||
ButtonLike::new("toggle outline view")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.child(h_stack().gap_1().children(breadcrumbs))
|
||||
// We disable the button when the containing pane is not focused:
|
||||
// Because right now all the breadcrumb does is open the outline view, which is an
|
||||
// action which operates on the active editor, clicking the breadcrumbs of another
|
||||
// editor could cause weirdness. I remember that at one point it actually caused a
|
||||
// panic weirdly.
|
||||
//
|
||||
// It might be possible that with changes around how focus is managed that we
|
||||
// might be able to update the active editor to the one with the breadcrumbs
|
||||
// clicked on? That or we could just add a code path for being able to open the
|
||||
// outline for a specific editor. Long term we'd like for it to be an actual
|
||||
// breadcrumb bar so that problem goes away
|
||||
//
|
||||
// — Julia (https://github.com/zed-industries/zed/pull/3505#pullrequestreview-1766198050)
|
||||
.disabled(!self.pane_focused)
|
||||
.on_click(cx.listener(|breadcrumbs, _, cx| {
|
||||
if let Some(workspace) = breadcrumbs.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
outline::toggle(workspace, &outline::Toggle, cx)
|
||||
})
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarItemView for Breadcrumbs {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
cx.notify();
|
||||
self.active_item = None;
|
||||
if let Some(item) = active_pane_item {
|
||||
let this = cx.view().downgrade();
|
||||
self.subscription = Some(item.subscribe_to_item_events(
|
||||
cx,
|
||||
Box::new(move |event, cx| {
|
||||
if let ItemEvent::UpdateBreadcrumbs = event {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(Event::UpdateLocation);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
));
|
||||
self.active_item = Some(item.boxed_clone());
|
||||
item.breadcrumb_location(cx)
|
||||
} else {
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
// fn location_for_event(
|
||||
// &self,
|
||||
// _: &Event,
|
||||
// current_location: ToolbarItemLocation,
|
||||
// cx: &AppContext,
|
||||
// ) -> ToolbarItemLocation {
|
||||
// if let Some(active_item) = self.active_item.as_ref() {
|
||||
// active_item.breadcrumb_location(cx)
|
||||
// } else {
|
||||
// current_location
|
||||
// }
|
||||
// }
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
self.pane_focused = pane_focused;
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,13 @@ util = { path = "../util" }
|
||||
anyhow.workspace = true
|
||||
async-broadcast = "0.4"
|
||||
futures.workspace = true
|
||||
image = "0.23"
|
||||
postage.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { package = "client2", path = "../client2", features = ["test-support"] }
|
||||
|
||||
@@ -464,7 +464,7 @@ impl ActiveCall {
|
||||
&self.pending_invites
|
||||
}
|
||||
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
|
||||
pub fn report_call_event(&self, operation: &'static str, cx: &mut 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);
|
||||
@@ -477,7 +477,7 @@ pub fn report_call_event_for_room(
|
||||
room_id: u64,
|
||||
channel_id: Option<u64>,
|
||||
client: &Arc<Client>,
|
||||
cx: &AppContext,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let telemetry = client.telemetry();
|
||||
let telemetry_settings = *TelemetrySettings::get_global(cx);
|
||||
|
||||
@@ -4,7 +4,7 @@ use client::{proto, User};
|
||||
use collections::HashMap;
|
||||
use gpui::WeakModel;
|
||||
pub use live_kit_client::Frame;
|
||||
use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ use live_kit_client::{
|
||||
};
|
||||
use postage::{sink::Sink, stream::Stream, watch};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use settings::Settings as _;
|
||||
use std::{future::Future, mem, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
|
||||
@@ -1267,7 +1267,6 @@ impl Room {
|
||||
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
|
||||
.await
|
||||
};
|
||||
|
||||
let publication = publish_track.await;
|
||||
this.upgrade()
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
|
||||
@@ -8,7 +8,8 @@ use collections::{hash_map, HashMap, HashSet};
|
||||
use db::RELEASE_CHANNEL;
|
||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task,
|
||||
WeakModel,
|
||||
};
|
||||
use rpc::{
|
||||
proto::{self, ChannelVisibility},
|
||||
@@ -46,7 +47,7 @@ pub struct ChannelStore {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Channel {
|
||||
pub id: ChannelId,
|
||||
pub name: String,
|
||||
pub name: SharedString,
|
||||
pub visibility: proto::ChannelVisibility,
|
||||
pub role: proto::ChannelRole,
|
||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||
@@ -895,14 +896,16 @@ impl ChannelStore {
|
||||
.channel_invitations
|
||||
.binary_search_by_key(&channel.id, |c| c.id)
|
||||
{
|
||||
Ok(ix) => Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name,
|
||||
Ok(ix) => {
|
||||
Arc::make_mut(&mut self.channel_invitations[ix]).name = channel.name.into()
|
||||
}
|
||||
Err(ix) => self.channel_invitations.insert(
|
||||
ix,
|
||||
Arc::new(Channel {
|
||||
id: channel.id,
|
||||
visibility: channel.visibility(),
|
||||
role: channel.role(),
|
||||
name: channel.name,
|
||||
name: channel.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel.parent_path,
|
||||
|
||||
@@ -104,7 +104,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
existing_channel.role = channel_proto.role();
|
||||
existing_channel.name = channel_proto.name;
|
||||
existing_channel.name = channel_proto.name.into();
|
||||
} else {
|
||||
self.channels_by_id.insert(
|
||||
channel_proto.id,
|
||||
@@ -112,7 +112,7 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||
id: channel_proto.id,
|
||||
visibility: channel_proto.visibility(),
|
||||
role: channel_proto.role(),
|
||||
name: channel_proto.name,
|
||||
name: channel_proto.name.into(),
|
||||
unseen_note_version: None,
|
||||
unseen_message_id: None,
|
||||
parent_path: channel_proto.parent_path,
|
||||
@@ -146,11 +146,11 @@ fn channel_path_sorting_key<'a>(
|
||||
let (parent_path, name) = channels_by_id
|
||||
.get(&id)
|
||||
.map_or((&[] as &[_], None), |channel| {
|
||||
(channel.parent_path.as_slice(), Some(channel.name.as_str()))
|
||||
(channel.parent_path.as_slice(), Some(channel.name.as_ref()))
|
||||
});
|
||||
parent_path
|
||||
.iter()
|
||||
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_str()))
|
||||
.filter_map(|id| Some(channels_by_id.get(id)?.name.as_ref()))
|
||||
.chain(name)
|
||||
}
|
||||
|
||||
|
||||
@@ -346,7 +346,7 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TelemetrySettings {
|
||||
pub diagnostics: bool,
|
||||
pub metrics: bool,
|
||||
|
||||
@@ -109,6 +109,10 @@ pub enum ClickhouseEvent {
|
||||
virtual_memory_in_bytes: u64,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
App {
|
||||
operation: &'static str,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -168,13 +172,8 @@ impl Telemetry {
|
||||
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 {
|
||||
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
||||
@@ -256,7 +255,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_copilot_event(
|
||||
@@ -273,7 +272,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(
|
||||
@@ -290,7 +289,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_call_event(
|
||||
@@ -307,7 +306,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_cpu_event(
|
||||
@@ -322,7 +321,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_memory_event(
|
||||
@@ -337,7 +336,21 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
// app_events are called at app open and app close, so flush is set to immediately send
|
||||
pub fn report_app_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
operation: &'static str,
|
||||
) {
|
||||
let event = ClickhouseEvent::App {
|
||||
operation,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings, true)
|
||||
}
|
||||
|
||||
fn milliseconds_since_first_event(&self) -> i64 {
|
||||
@@ -358,6 +371,7 @@ impl Telemetry {
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
immediate_flush: bool,
|
||||
) {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
@@ -370,7 +384,7 @@ impl Telemetry {
|
||||
.push(ClickhouseEventWrapper { signed_in, event });
|
||||
|
||||
if state.installation_id.is_some() {
|
||||
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
|
||||
@@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings {
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||
pub fn new(http: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
@@ -551,7 +551,6 @@ impl Client {
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
let message_type_id = TypeId::of::<M>();
|
||||
|
||||
let mut state = self.state.write();
|
||||
state
|
||||
.models_by_message_type
|
||||
@@ -694,8 +693,8 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
read_credentials_from_keychain(cx).await.is_some()
|
||||
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
read_credentials_from_keychain(cx).is_some()
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
@@ -726,7 +725,7 @@ impl Client {
|
||||
let mut read_from_keychain = false;
|
||||
let mut credentials = self.state.read().credentials.clone();
|
||||
if credentials.is_none() && try_keychain {
|
||||
credentials = read_credentials_from_keychain(cx).await;
|
||||
credentials = read_credentials_from_keychain(cx);
|
||||
read_from_keychain = credentials.is_some();
|
||||
}
|
||||
if credentials.is_none() {
|
||||
@@ -1325,7 +1324,7 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
||||
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
||||
if IMPERSONATE_LOGIN.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::Future;
|
||||
use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
@@ -107,6 +108,10 @@ pub enum ClickhouseEvent {
|
||||
virtual_memory_in_bytes: u64,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
App {
|
||||
operation: &'static str,
|
||||
milliseconds_since_first_event: i64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -122,12 +127,13 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30);
|
||||
|
||||
impl Telemetry {
|
||||
pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||
pub fn new(client: Arc<dyn HttpClient>, cx: &mut 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,
|
||||
@@ -147,9 +153,30 @@ impl Telemetry {
|
||||
}),
|
||||
});
|
||||
|
||||
// We should only ever have one instance of Telemetry, leak the subscription to keep it alive
|
||||
// rather than store in TelemetryState, complicating spawn as subscriptions are not Send
|
||||
std::mem::forget(cx.on_app_quit({
|
||||
let this = this.clone();
|
||||
move |cx| this.shutdown_telemetry(cx)
|
||||
}));
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn shutdown_telemetry(self: &Arc<Self>, _: &mut AppContext) -> impl Future<Output = ()> {
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
// Skip calling this function in tests.
|
||||
// TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
fn shutdown_telemetry(self: &Arc<Self>, cx: &mut AppContext) -> impl Future<Output = ()> {
|
||||
let telemetry_settings = TelemetrySettings::get_global(cx).clone();
|
||||
self.report_app_event(telemetry_settings, "close");
|
||||
Task::ready(())
|
||||
}
|
||||
|
||||
pub fn log_file_path(&self) -> Option<PathBuf> {
|
||||
Some(self.state.lock().log_file.as_ref()?.path().to_path_buf())
|
||||
}
|
||||
@@ -163,13 +190,8 @@ impl Telemetry {
|
||||
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 {
|
||||
// Avoiding calling `System::new_all()`, as there have been crashes related to it
|
||||
@@ -257,7 +279,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_copilot_event(
|
||||
@@ -274,7 +296,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_assistant_event(
|
||||
@@ -291,7 +313,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_call_event(
|
||||
@@ -308,7 +330,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_cpu_event(
|
||||
@@ -323,7 +345,7 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
pub fn report_memory_event(
|
||||
@@ -338,7 +360,21 @@ impl Telemetry {
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings)
|
||||
self.report_clickhouse_event(event, telemetry_settings, false)
|
||||
}
|
||||
|
||||
// app_events are called at app open and app close, so flush is set to immediately send
|
||||
pub fn report_app_event(
|
||||
self: &Arc<Self>,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
operation: &'static str,
|
||||
) {
|
||||
let event = ClickhouseEvent::App {
|
||||
operation,
|
||||
milliseconds_since_first_event: self.milliseconds_since_first_event(),
|
||||
};
|
||||
|
||||
self.report_clickhouse_event(event, telemetry_settings, true)
|
||||
}
|
||||
|
||||
fn milliseconds_since_first_event(&self) -> i64 {
|
||||
@@ -359,6 +395,7 @@ impl Telemetry {
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
immediate_flush: bool,
|
||||
) {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
@@ -371,7 +408,7 @@ impl Telemetry {
|
||||
.push(ClickhouseEventWrapper { signed_in, event });
|
||||
|
||||
if state.installation_id.is_some() {
|
||||
if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.28.0"
|
||||
version = "0.30.1"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -116,12 +116,13 @@ struct CreateUserResponse {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Panic {
|
||||
version: String,
|
||||
release_channel: String,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[instrument(skip(panic))]
|
||||
async fn trace_panic(panic: Json<Panic>) -> Result<()> {
|
||||
tracing::error!(version = %panic.version, text = %panic.text, "panic report");
|
||||
tracing::error!(version = %panic.version, release_channel = %panic.release_channel, text = %panic.text, "panic report");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1220,6 +1220,13 @@ impl Database {
|
||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||
}
|
||||
|
||||
new_parent_path = new_parent.path();
|
||||
new_parent_channel = Some(new_parent);
|
||||
} else {
|
||||
|
||||
@@ -450,6 +450,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Can't move a channel into its ancestor
|
||||
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
|
||||
@@ -4,8 +4,10 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica
|
||||
use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
|
||||
use live_kit_client::MacOSDisplay;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use rpc::proto::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use workspace::{
|
||||
dock::{test::TestPanel, DockPosition},
|
||||
@@ -1602,6 +1604,141 @@ async fn test_following_across_workspaces(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_following_into_excluded_file(
|
||||
deterministic: Arc<Deterministic>,
|
||||
mut cx_a: &mut TestAppContext,
|
||||
mut cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
for cx in [&mut cx_a, &mut cx_b] {
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".git": {
|
||||
"COMMIT_EDITMSG": "write your commit message here",
|
||||
},
|
||||
"1.txt": "one\none\none",
|
||||
"2.txt": "two\ntwo\ntwo",
|
||||
"3.txt": "three\nthree\nthree",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
let workspace_a = window_a.root(cx_a);
|
||||
let peer_id_a = client_a.peer_id().unwrap();
|
||||
let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
let workspace_b = window_b.root(cx_b);
|
||||
|
||||
// Client A opens editors for a regular file and an excluded file.
|
||||
let editor_for_regular = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let editor_for_excluded_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
// Client A updates their selections in those editors
|
||||
editor_for_regular.update(cx_a, |editor, cx| {
|
||||
editor.handle_input("a", cx);
|
||||
editor.handle_input("b", cx);
|
||||
editor.handle_input("c", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
});
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
editor.select_all(&Default::default(), cx);
|
||||
editor.handle_input("new commit message", cx);
|
||||
editor.select_left(&Default::default(), cx);
|
||||
assert_eq!(editor.selections.ranges(cx), vec![18..17]);
|
||||
});
|
||||
|
||||
// When client B starts following client A, currently visible file is replicated
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.follow(peer_id_a, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap()
|
||||
});
|
||||
assert_eq!(
|
||||
cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
|
||||
Some((worktree_id, ".git/COMMIT_EDITMSG").into())
|
||||
);
|
||||
assert_eq!(
|
||||
editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
vec![18..17]
|
||||
);
|
||||
|
||||
// Changes from B to the excluded file are replicated in A's editor
|
||||
editor_for_excluded_b.update(cx_b, |editor, cx| {
|
||||
editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"new commit messag\nCo-Authored-By: B <b@b.b>"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_push_notifications(
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
|
||||
@@ -2981,11 +2981,10 @@ async fn test_fs_operations(
|
||||
|
||||
let entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
assert_eq!(
|
||||
@@ -3010,7 +3009,6 @@ async fn test_fs_operations(
|
||||
.update(cx_b, |project, cx| {
|
||||
project.rename_entry(entry.id, Path::new("d.txt"), cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3034,11 +3032,10 @@ async fn test_fs_operations(
|
||||
|
||||
let dir_entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
assert_eq!(
|
||||
@@ -3061,25 +3058,19 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3120,9 +3111,7 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
.unwrap()
|
||||
project.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -3941,7 +3930,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
// Ensure client B observes the new diagnostics.
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
&[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -3961,14 +3950,14 @@ async fn test_collaborating_with_diagnostics(
|
||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
let project_c_diagnostic_summaries =
|
||||
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>()
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
|
||||
})));
|
||||
project_c.update(cx_c, |_, cx| {
|
||||
let summaries = project_c_diagnostic_summaries.clone();
|
||||
cx.subscribe(&project_c, {
|
||||
move |p, _, event, cx| {
|
||||
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||
*summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
|
||||
*summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -4018,7 +4007,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
deterministic.run_until_parked();
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -4034,7 +4023,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
});
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -4097,13 +4086,22 @@ async fn test_collaborating_with_diagnostics(
|
||||
);
|
||||
deterministic.run_until_parked();
|
||||
project_a.read_with(cx_a, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
project
|
||||
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
|
||||
.unwrap()
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ publish = false
|
||||
name = "collab2"
|
||||
|
||||
[[bin]]
|
||||
name = "seed"
|
||||
name = "seed2"
|
||||
required-features = ["seed-support"]
|
||||
|
||||
[dependencies]
|
||||
@@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
||||
|
||||
collab_ui = { path = "../collab_ui", features = ["test-support"] }
|
||||
collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] }
|
||||
|
||||
async-trait.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
|
||||
@@ -1220,6 +1220,13 @@ impl Database {
|
||||
self.check_user_is_channel_admin(&new_parent, admin_id, &*tx)
|
||||
.await?;
|
||||
|
||||
if new_parent
|
||||
.ancestors_including_self()
|
||||
.any(|id| id == channel.id)
|
||||
{
|
||||
Err(anyhow!("cannot move a channel into one of its descendants"))?;
|
||||
}
|
||||
|
||||
new_parent_path = new_parent.path();
|
||||
new_parent_channel = Some(new_parent);
|
||||
} else {
|
||||
|
||||
@@ -420,8 +420,6 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Dag is: zed - projects - livestreaming
|
||||
|
||||
// Move to same parent should be a no-op
|
||||
assert!(db
|
||||
.move_channel(projects_id, Some(zed_id), user_id)
|
||||
@@ -450,6 +448,20 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Can't move a channel into its ancestor
|
||||
db.move_channel(projects_id, Some(livestreaming_id), user_id)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let result = db.get_channels_for_user(user_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(zed_id, &[]),
|
||||
(projects_id, &[]),
|
||||
(livestreaming_id, &[projects_id]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
|
||||
@@ -7,7 +7,7 @@ use call::ActiveCall;
|
||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||
use client::User;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{BackgroundExecutor, Model, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, Model, SharedString, TestAppContext};
|
||||
use rpc::{
|
||||
proto::{self, ChannelRole},
|
||||
RECEIVE_TIMEOUT,
|
||||
@@ -46,13 +46,13 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
@@ -92,7 +92,7 @@ async fn test_core_channels(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
@@ -140,13 +140,13 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
@@ -168,19 +168,19 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 0,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 1,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".to_string(),
|
||||
name: "channel-c".into(),
|
||||
role: ChannelRole::Member,
|
||||
depth: 2,
|
||||
},
|
||||
@@ -211,19 +211,19 @@ async fn test_core_channels(
|
||||
&[
|
||||
ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_b_id,
|
||||
name: "channel-b".to_string(),
|
||||
name: "channel-b".into(),
|
||||
depth: 1,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
id: channel_c_id,
|
||||
name: "channel-c".to_string(),
|
||||
name: "channel-c".into(),
|
||||
depth: 2,
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
@@ -245,7 +245,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -255,7 +255,7 @@ async fn test_core_channels(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -278,7 +278,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a".to_string(),
|
||||
name: "channel-a".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -309,7 +309,7 @@ async fn test_core_channels(
|
||||
cx_a,
|
||||
&[ExpectedChannel {
|
||||
id: channel_a_id,
|
||||
name: "channel-a-renamed".to_string(),
|
||||
name: "channel-a-renamed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
@@ -418,7 +418,7 @@ async fn test_channel_room(
|
||||
cx_b,
|
||||
&[ExpectedChannel {
|
||||
id: zed_id,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
depth: 0,
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
@@ -680,7 +680,7 @@ async fn test_permissions_update_while_invited(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -708,7 +708,7 @@ async fn test_permissions_update_while_invited(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust".to_string(),
|
||||
name: "rust".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -747,7 +747,7 @@ async fn test_channel_rename(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".to_string(),
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Admin,
|
||||
}],
|
||||
);
|
||||
@@ -759,7 +759,7 @@ async fn test_channel_rename(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: rust_id,
|
||||
name: "rust-archive".to_string(),
|
||||
name: "rust-archive".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -888,7 +888,7 @@ async fn test_lost_channel_creation(
|
||||
&[ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
}],
|
||||
);
|
||||
@@ -912,13 +912,13 @@ async fn test_lost_channel_creation(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".to_string(),
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Admin,
|
||||
},
|
||||
],
|
||||
@@ -943,13 +943,13 @@ async fn test_lost_channel_creation(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: channel_id,
|
||||
name: "x".to_string(),
|
||||
name: "x".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: subchannel_id,
|
||||
name: "subchannel".to_string(),
|
||||
name: "subchannel".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
@@ -1221,13 +1221,13 @@ async fn test_channel_membership_notifications(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".to_string(),
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Member,
|
||||
},
|
||||
],
|
||||
@@ -1250,13 +1250,13 @@ async fn test_channel_membership_notifications(
|
||||
ExpectedChannel {
|
||||
depth: 0,
|
||||
id: zed_channel,
|
||||
name: "zed".to_string(),
|
||||
name: "zed".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
ExpectedChannel {
|
||||
depth: 1,
|
||||
id: vim_channel,
|
||||
name: "vim".to_string(),
|
||||
name: "vim".into(),
|
||||
role: ChannelRole::Guest,
|
||||
},
|
||||
],
|
||||
@@ -1476,7 +1476,7 @@ async fn test_channel_moving(
|
||||
struct ExpectedChannel {
|
||||
depth: usize,
|
||||
id: ChannelId,
|
||||
name: String,
|
||||
name: SharedString,
|
||||
role: ChannelRole,
|
||||
}
|
||||
|
||||
@@ -1515,7 +1515,7 @@ fn assert_channels(
|
||||
.ordered_channels()
|
||||
.map(|(depth, channel)| ExpectedChannel {
|
||||
depth,
|
||||
name: channel.name.clone(),
|
||||
name: channel.name.clone().into(),
|
||||
id: channel.id,
|
||||
role: channel.role,
|
||||
})
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
// use call::ActiveCall;
|
||||
// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
|
||||
// use editor::{Editor, ExcerptRange, MultiBuffer};
|
||||
// use gpui::{BackgroundExecutor, TestAppContext, View};
|
||||
// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext};
|
||||
// use live_kit_client::MacOSDisplay;
|
||||
// use project::project_settings::ProjectSettings;
|
||||
// use rpc::proto::PeerId;
|
||||
// use serde_json::json;
|
||||
// use settings::SettingsStore;
|
||||
// use std::borrow::Cow;
|
||||
// use workspace::{
|
||||
// dock::{test::TestPanel, DockPosition},
|
||||
@@ -24,7 +26,7 @@
|
||||
// cx_c: &mut TestAppContext,
|
||||
// cx_d: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -71,12 +73,22 @@
|
||||
// .unwrap();
|
||||
|
||||
// let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
// let workspace_a = window_a.root(cx_a);
|
||||
// let workspace_a = window_a.root(cx_a).unwrap();
|
||||
// let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
// let workspace_b = window_b.root(cx_b);
|
||||
// let workspace_b = window_b.root(cx_b).unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c);
|
||||
// let cx_c = &mut cx_c;
|
||||
// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d);
|
||||
// let cx_d = &mut cx_d;
|
||||
|
||||
// // Client A opens some editors.
|
||||
// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let editor_a1 = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -132,8 +144,8 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// cx_c.executor().run_until_parked();
|
||||
// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -145,19 +157,19 @@
|
||||
// Some((worktree_id, "2.txt").into())
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![2..1]
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![3..2]
|
||||
// );
|
||||
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// let active_call_c = cx_c.read(ActiveCall::global);
|
||||
// let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
// let window_c = client_c.build_workspace(&project_c, cx_c);
|
||||
// let workspace_c = window_c.root(cx_c);
|
||||
// let workspace_c = window_c.root(cx_c).unwrap();
|
||||
// active_call_c
|
||||
// .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
|
||||
// .await
|
||||
@@ -172,10 +184,13 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// cx_d.foreground().run_until_parked();
|
||||
// cx_d.executor().run_until_parked();
|
||||
// let active_call_d = cx_d.read(ActiveCall::global);
|
||||
// let project_d = client_d.build_remote_project(project_id, cx_d).await;
|
||||
// let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
|
||||
// let workspace_d = client_d
|
||||
// .build_workspace(&project_d, cx_d)
|
||||
// .root(cx_d)
|
||||
// .unwrap();
|
||||
// active_call_d
|
||||
// .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
|
||||
// .await
|
||||
@@ -183,7 +198,7 @@
|
||||
// drop(project_d);
|
||||
|
||||
// // All clients see that clients B and C are following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -198,7 +213,7 @@
|
||||
// });
|
||||
|
||||
// // All clients see that clients B is following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -216,7 +231,7 @@
|
||||
// .unwrap();
|
||||
|
||||
// // All clients see that clients B and C are following client A.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -240,7 +255,7 @@
|
||||
// .unwrap();
|
||||
|
||||
// // All clients see that D is following C
|
||||
// cx_d.foreground().run_until_parked();
|
||||
// cx_d.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -257,7 +272,7 @@
|
||||
// cx_c.drop_last(workspace_c);
|
||||
|
||||
// // Clients A and B see that client B is following A, and client C is not present in the followers.
|
||||
// cx_c.foreground().run_until_parked();
|
||||
// cx_c.executor().run_until_parked();
|
||||
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
|
||||
// assert_eq!(
|
||||
// followers_by_leader(project_id, cx),
|
||||
@@ -271,12 +286,15 @@
|
||||
// workspace.activate_item(&editor_a1, cx)
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// // When client A opens a multibuffer, client B does so as well.
|
||||
// let multibuffer_a = cx_a.add_model(|cx| {
|
||||
// let multibuffer_a = cx_a.build_model(|cx| {
|
||||
// let buffer_a1 = project_a.update(cx, |project, cx| {
|
||||
// project
|
||||
// .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
|
||||
@@ -308,12 +326,12 @@
|
||||
// });
|
||||
// let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
|
||||
// let editor =
|
||||
// cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
|
||||
// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
|
||||
// workspace.add_item(Box::new(editor.clone()), cx);
|
||||
// editor
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -321,8 +339,8 @@
|
||||
// .unwrap()
|
||||
// });
|
||||
// assert_eq!(
|
||||
// multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
|
||||
// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
|
||||
// );
|
||||
|
||||
// // When client A navigates back and forth, client B does so as well.
|
||||
@@ -333,8 +351,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// workspace_a
|
||||
@@ -344,8 +365,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b2.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// workspace_a
|
||||
@@ -355,8 +379,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
// });
|
||||
|
||||
// // Changes to client A's editor are reflected on client B.
|
||||
@@ -364,20 +391,20 @@
|
||||
// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| {
|
||||
// editor_b1.update(cx_b, |editor, cx| {
|
||||
// assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
|
||||
// });
|
||||
|
||||
// editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
|
||||
|
||||
// editor_a1.update(cx_a, |editor, cx| {
|
||||
// editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
|
||||
// editor.set_scroll_position(vec2f(0., 100.), cx);
|
||||
// editor.set_scroll_position(point(0., 100.), cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_b1.read_with(cx_b, |editor, cx| {
|
||||
// editor_b1.update(cx_b, |editor, cx| {
|
||||
// assert_eq!(editor.selections.ranges(cx), &[3..3]);
|
||||
// });
|
||||
|
||||
@@ -390,11 +417,11 @@
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, cx| workspace
|
||||
// workspace_b.update(cx_b, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// editor_b1.id()
|
||||
// .item_id()),
|
||||
// editor_b1.item_id()
|
||||
// );
|
||||
|
||||
// // Client A starts following client B.
|
||||
@@ -405,15 +432,15 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// Some(peer_id_b)
|
||||
// );
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// editor_a1.id()
|
||||
// .item_id()),
|
||||
// editor_a1.item_id()
|
||||
// );
|
||||
|
||||
// // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
|
||||
@@ -432,7 +459,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .expect("no active item")
|
||||
@@ -446,8 +473,11 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// editor_a1.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
|
||||
@@ -455,26 +485,26 @@
|
||||
// workspace.activate_item(&multibuffer_editor_b, cx)
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().id(),
|
||||
// multibuffer_editor_a.id()
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// multibuffer_editor_a.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
|
||||
// let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
|
||||
// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left));
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace.add_panel(panel, cx);
|
||||
// workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// shared_screen.id()
|
||||
// .item_id()),
|
||||
// shared_screen.item_id()
|
||||
// );
|
||||
|
||||
// // Toggling the focus back to the pane causes client A to return to the multibuffer.
|
||||
@@ -482,16 +512,16 @@
|
||||
// workspace.toggle_panel_focus::<TestPanel>(cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| {
|
||||
// workspace_a.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(
|
||||
// workspace.active_item(cx).unwrap().id(),
|
||||
// multibuffer_editor_a.id()
|
||||
// workspace.active_item(cx).unwrap().item_id(),
|
||||
// multibuffer_editor_a.item_id()
|
||||
// )
|
||||
// });
|
||||
|
||||
// // Client B activates an item that doesn't implement following,
|
||||
// // so the previously-opened screen-sharing item gets activated.
|
||||
// let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
|
||||
// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new());
|
||||
// workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace.active_pane().update(cx, |pane, cx| {
|
||||
// pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
|
||||
@@ -499,18 +529,18 @@
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, cx| workspace
|
||||
// workspace_a.update(cx_a, |workspace, cx| workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .id()),
|
||||
// shared_screen.id()
|
||||
// .item_id()),
|
||||
// shared_screen.item_id()
|
||||
// );
|
||||
|
||||
// // Following interrupts when client B disconnects.
|
||||
// client_b.disconnect(&cx_b.to_async());
|
||||
// executor.advance_clock(RECONNECT_TIMEOUT);
|
||||
// assert_eq!(
|
||||
// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
|
||||
// None
|
||||
// );
|
||||
// }
|
||||
@@ -521,7 +551,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// server
|
||||
@@ -560,13 +590,19 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
// let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||
// let client_b_id = project_a.update(cx_a, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
|
||||
@@ -584,7 +620,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
|
||||
// let pane_paths = |pane: &View<workspace::Pane>, cx: &mut TestAppContext| {
|
||||
// pane.update(cx, |pane, cx| {
|
||||
// pane.items()
|
||||
// .map(|item| {
|
||||
@@ -642,7 +678,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// server
|
||||
@@ -685,7 +721,10 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client A opens a file.
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -696,7 +735,10 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client B opens a different file.
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// workspace_b
|
||||
// .update(cx_b, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
@@ -1167,7 +1209,7 @@
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// // 2 clients connect to a server.
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// server
|
||||
@@ -1207,8 +1249,17 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// // Client A opens some editors.
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let _editor_a1 = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
@@ -1219,9 +1270,12 @@
|
||||
// .unwrap();
|
||||
|
||||
// // Client B starts following client A.
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let leader_id = project_b.read_with(cx_b, |project, _| {
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
// let leader_id = project_b.update(cx_b, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
// workspace_b
|
||||
@@ -1231,10 +1285,10 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
|
||||
// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
@@ -1245,7 +1299,7 @@
|
||||
// // When client B moves, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1256,14 +1310,14 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// // When client B edits, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1274,16 +1328,16 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// // When client B scrolls, it automatically stops following client A.
|
||||
// editor_b2.update(cx_b, |editor, cx| {
|
||||
// editor.set_scroll_position(vec2f(0., 3.), cx)
|
||||
// editor.set_scroll_position(point(0., 3.), cx)
|
||||
// });
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
|
||||
@@ -1294,7 +1348,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
@@ -1303,13 +1357,13 @@
|
||||
// workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
|
||||
// });
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// Some(leader_id)
|
||||
// );
|
||||
|
||||
@@ -1321,7 +1375,7 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
|
||||
// None
|
||||
// );
|
||||
// }
|
||||
@@ -1332,7 +1386,7 @@
|
||||
// cx_a: &mut TestAppContext,
|
||||
// cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// server
|
||||
@@ -1345,20 +1399,26 @@
|
||||
|
||||
// client_a.fs().insert_tree("/a", json!({})).await;
|
||||
// let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let project_id = active_call_a
|
||||
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// executor.run_until_parked();
|
||||
// let client_a_id = project_b.read_with(cx_b, |project, _| {
|
||||
// let client_a_id = project_b.update(cx_b, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
// let client_b_id = project_a.read_with(cx_a, |project, _| {
|
||||
// let client_b_id = project_a.update(cx_a, |project, _| {
|
||||
// project.collaborators().values().next().unwrap().peer_id
|
||||
// });
|
||||
|
||||
@@ -1370,13 +1430,13 @@
|
||||
// });
|
||||
|
||||
// futures::try_join!(a_follow_b, b_follow_a).unwrap();
|
||||
// workspace_a.read_with(cx_a, |workspace, _| {
|
||||
// workspace_a.update(cx_a, |workspace, _| {
|
||||
// assert_eq!(
|
||||
// workspace.leader_for_pane(workspace.active_pane()),
|
||||
// Some(client_b_id)
|
||||
// );
|
||||
// });
|
||||
// workspace_b.read_with(cx_b, |workspace, _| {
|
||||
// workspace_b.update(cx_b, |workspace, _| {
|
||||
// assert_eq!(
|
||||
// workspace.leader_for_pane(workspace.active_pane()),
|
||||
// Some(client_a_id)
|
||||
@@ -1398,7 +1458,7 @@
|
||||
// // b opens a different file in project 2, a follows b
|
||||
// // b opens a different file in project 1, a cannot follow b
|
||||
// // b shares the project, a joins the project and follows b
|
||||
// let mut server = TestServer::start(&executor).await;
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// cx_a.update(editor::init);
|
||||
@@ -1435,8 +1495,14 @@
|
||||
// let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
|
||||
// let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
|
||||
|
||||
// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
|
||||
// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
|
||||
// let workspace_a = client_a
|
||||
// .build_workspace(&project_a, cx_a)
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
// let workspace_b = client_b
|
||||
// .build_workspace(&project_b, cx_b)
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
|
||||
// cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
|
||||
@@ -1455,6 +1521,12 @@
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
|
||||
@@ -1476,11 +1548,12 @@
|
||||
// let workspace_b_project_a = cx_b
|
||||
// .windows()
|
||||
// .iter()
|
||||
// .max_by_key(|window| window.id())
|
||||
// .max_by_key(|window| window.item_id())
|
||||
// .unwrap()
|
||||
// .downcast::<Workspace>()
|
||||
// .unwrap()
|
||||
// .root(cx_b);
|
||||
// .root(cx_b)
|
||||
// .unwrap();
|
||||
|
||||
// // assert that b is following a in project a in w.rs
|
||||
// workspace_b_project_a.update(cx_b, |workspace, cx| {
|
||||
@@ -1534,7 +1607,7 @@
|
||||
// workspace.leader_for_pane(workspace.active_pane())
|
||||
// );
|
||||
// let item = workspace.active_pane().read(cx).active_item().unwrap();
|
||||
// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
|
||||
// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into());
|
||||
// });
|
||||
|
||||
// // b moves to y.rs in b's project, a is still following but can't yet see
|
||||
@@ -1578,11 +1651,12 @@
|
||||
// let workspace_a_project_b = cx_a
|
||||
// .windows()
|
||||
// .iter()
|
||||
// .max_by_key(|window| window.id())
|
||||
// .max_by_key(|window| window.item_id())
|
||||
// .unwrap()
|
||||
// .downcast::<Workspace>()
|
||||
// .unwrap()
|
||||
// .root(cx_a);
|
||||
// .root(cx_a)
|
||||
// .unwrap();
|
||||
|
||||
// workspace_a_project_b.update(cx_a, |workspace, cx| {
|
||||
// assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
|
||||
@@ -1596,12 +1670,151 @@
|
||||
// });
|
||||
// }
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_following_into_excluded_file(
|
||||
// executor: BackgroundExecutor,
|
||||
// mut cx_a: &mut TestAppContext,
|
||||
// mut cx_b: &mut TestAppContext,
|
||||
// ) {
|
||||
// let mut server = TestServer::start(executor.clone()).await;
|
||||
// let client_a = server.create_client(cx_a, "user_a").await;
|
||||
// let client_b = server.create_client(cx_b, "user_b").await;
|
||||
// for cx in [&mut cx_a, &mut cx_b] {
|
||||
// cx.update(|cx| {
|
||||
// cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||
// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// server
|
||||
// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
// .await;
|
||||
// let active_call_a = cx_a.read(ActiveCall::global);
|
||||
// let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
// cx_a.update(editor::init);
|
||||
// cx_b.update(editor::init);
|
||||
|
||||
// client_a
|
||||
// .fs()
|
||||
// .insert_tree(
|
||||
// "/a",
|
||||
// json!({
|
||||
// ".git": {
|
||||
// "COMMIT_EDITMSG": "write your commit message here",
|
||||
// },
|
||||
// "1.txt": "one\none\none",
|
||||
// "2.txt": "two\ntwo\ntwo",
|
||||
// "3.txt": "three\nthree\nthree",
|
||||
// }),
|
||||
// )
|
||||
// .await;
|
||||
// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
// active_call_a
|
||||
// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let project_id = active_call_a
|
||||
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
// active_call_b
|
||||
// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let window_a = client_a.build_workspace(&project_a, cx_a);
|
||||
// let workspace_a = window_a.root(cx_a).unwrap();
|
||||
// let peer_id_a = client_a.peer_id().unwrap();
|
||||
// let window_b = client_b.build_workspace(&project_b, cx_b);
|
||||
// let workspace_b = window_b.root(cx_b).unwrap();
|
||||
|
||||
// todo!("could be wrong")
|
||||
// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
|
||||
// let cx_a = &mut cx_a;
|
||||
// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
|
||||
// let cx_b = &mut cx_b;
|
||||
|
||||
// // Client A opens editors for a regular file and an excluded file.
|
||||
// let editor_for_regular = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap();
|
||||
// let editor_for_excluded_a = workspace_a
|
||||
// .update(cx_a, |workspace, cx| {
|
||||
// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
|
||||
// })
|
||||
// .await
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap();
|
||||
|
||||
// // Client A updates their selections in those editors
|
||||
// editor_for_regular.update(cx_a, |editor, cx| {
|
||||
// editor.handle_input("a", cx);
|
||||
// editor.handle_input("b", cx);
|
||||
// editor.handle_input("c", cx);
|
||||
// editor.select_left(&Default::default(), cx);
|
||||
// assert_eq!(editor.selections.ranges(cx), vec![3..2]);
|
||||
// });
|
||||
// editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
// editor.select_all(&Default::default(), cx);
|
||||
// editor.handle_input("new commit message", cx);
|
||||
// editor.select_left(&Default::default(), cx);
|
||||
// assert_eq!(editor.selections.ranges(cx), vec![18..17]);
|
||||
// });
|
||||
|
||||
// // When client B starts following client A, currently visible file is replicated
|
||||
// workspace_b
|
||||
// .update(cx_b, |workspace, cx| {
|
||||
// workspace.follow(peer_id_a, cx).unwrap()
|
||||
// })
|
||||
// .await
|
||||
// .unwrap();
|
||||
|
||||
// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
|
||||
// workspace
|
||||
// .active_item(cx)
|
||||
// .unwrap()
|
||||
// .downcast::<Editor>()
|
||||
// .unwrap()
|
||||
// });
|
||||
// assert_eq!(
|
||||
// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
|
||||
// Some((worktree_id, ".git/COMMIT_EDITMSG").into())
|
||||
// );
|
||||
// assert_eq!(
|
||||
// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
|
||||
// vec![18..17]
|
||||
// );
|
||||
|
||||
// // Changes from B to the excluded file are replicated in A's editor
|
||||
// editor_for_excluded_b.update(cx_b, |editor, cx| {
|
||||
// editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
|
||||
// });
|
||||
// executor.run_until_parked();
|
||||
// editor_for_excluded_a.update(cx_a, |editor, cx| {
|
||||
// assert_eq!(
|
||||
// editor.text(cx),
|
||||
// "new commit messag\nCo-Authored-By: B <b@b.b>"
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// fn visible_push_notifications(
|
||||
// cx: &mut TestAppContext,
|
||||
// ) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
|
||||
// ) -> Vec<gpui::View<ProjectSharedNotification>> {
|
||||
// let mut ret = Vec::new();
|
||||
// for window in cx.windows() {
|
||||
// window.read_with(cx, |window| {
|
||||
// window.update(cx, |window| {
|
||||
// if let Some(handle) = window
|
||||
// .root_view()
|
||||
// .clone()
|
||||
@@ -1645,8 +1858,8 @@
|
||||
// })
|
||||
// }
|
||||
|
||||
// fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
|
||||
// workspace.read_with(cx, |workspace, cx| {
|
||||
// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
|
||||
// workspace.update(cx, |workspace, cx| {
|
||||
// let active_pane = workspace.active_pane();
|
||||
// workspace
|
||||
// .panes()
|
||||
|
||||
@@ -2781,11 +2781,10 @@ async fn test_fs_operations(
|
||||
|
||||
let entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "c.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2812,8 +2811,8 @@ async fn test_fs_operations(
|
||||
.update(cx_b, |project, cx| {
|
||||
project.rename_entry(entry.id, Path::new("d.txt"), cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2838,11 +2837,10 @@ async fn test_fs_operations(
|
||||
|
||||
let dir_entry = project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2867,27 +2865,24 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
.unwrap()
|
||||
project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -2928,11 +2923,10 @@ async fn test_fs_operations(
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project
|
||||
.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
.unwrap()
|
||||
project.copy_entry(entry.id, Path::new("f.txt"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
worktree_a.read_with(cx_a, |worktree, _| {
|
||||
@@ -3685,7 +3679,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
&[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -3705,14 +3699,14 @@ async fn test_collaborating_with_diagnostics(
|
||||
let project_c = client_c.build_remote_project(project_id, cx_c).await;
|
||||
let project_c_diagnostic_summaries =
|
||||
Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>()
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>()
|
||||
})));
|
||||
project_c.update(cx_c, |_, cx| {
|
||||
let summaries = project_c_diagnostic_summaries.clone();
|
||||
cx.subscribe(&project_c, {
|
||||
move |p, _, event, cx| {
|
||||
if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
|
||||
*summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
|
||||
*summaries.borrow_mut() = p.diagnostic_summaries(false, cx).collect();
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -3763,7 +3757,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -3780,7 +3774,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(cx).collect::<Vec<_>>(),
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
@@ -3841,15 +3835,24 @@ async fn test_collaborating_with_diagnostics(
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
|
||||
project_b.read_with(cx_b, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
|
||||
project_c.read_with(cx_c, |project, cx| {
|
||||
assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
|
||||
assert_eq!(
|
||||
project.diagnostic_summaries(false, cx).collect::<Vec<_>>(),
|
||||
[]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::db::ChannelRole;
|
||||
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::{BackgroundExecutor, TestAppContext};
|
||||
use gpui::{BackgroundExecutor, SharedString, TestAppContext};
|
||||
use rand::prelude::*;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@@ -30,13 +30,13 @@ struct RandomChannelBufferTest;
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
enum ChannelBufferOperation {
|
||||
JoinChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
},
|
||||
LeaveChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
},
|
||||
EditChannelNotes {
|
||||
channel_name: String,
|
||||
channel_name: SharedString,
|
||||
edits: Vec<(Range<usize>, Arc<str>)>,
|
||||
},
|
||||
Noop,
|
||||
|
||||
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
ensure_project_shared(&project, client, cx).await;
|
||||
project
|
||||
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
|
||||
.unwrap()
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use client::{
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle};
|
||||
use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext};
|
||||
use language::LanguageRegistry;
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
|
||||
@@ -149,7 +149,7 @@ impl TestServer {
|
||||
.user_id
|
||||
};
|
||||
let client_name = name.to_string();
|
||||
let mut client = cx.read(|cx| Client::new(http.clone(), cx));
|
||||
let mut client = cx.update(|cx| Client::new(http.clone(), cx));
|
||||
let server = self.server.clone();
|
||||
let db = self.app_state.db.clone();
|
||||
let connection_killers = self.connection_killers.clone();
|
||||
@@ -602,14 +602,12 @@ impl TestClient {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
//todo(workspace)
|
||||
#[allow(dead_code)]
|
||||
pub fn build_workspace(
|
||||
&self,
|
||||
pub fn build_workspace<'a>(
|
||||
&'a self,
|
||||
project: &Model<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> WindowHandle<Workspace> {
|
||||
cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
cx: &'a mut TestAppContext,
|
||||
) -> (View<Workspace>, &'a mut VisualTestContext) {
|
||||
cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ notifications = { package = "notifications2", path = "../notifications2" }
|
||||
rich_text = { package = "rich_text2", path = "../rich_text2" }
|
||||
picker = { package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
# recent_projects = { path = "../recent_projects" }
|
||||
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
||||
rpc = { package ="rpc2", path = "../rpc2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
|
||||
|
||||
@@ -1,454 +1,444 @@
|
||||
// use anyhow::{anyhow, Result};
|
||||
// use call::report_call_event_for_channel;
|
||||
// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
// use client::{
|
||||
// proto::{self, PeerId},
|
||||
// Collaborator, ParticipantIndex,
|
||||
// };
|
||||
// use collections::HashMap;
|
||||
// use editor::{CollaborationHub, Editor};
|
||||
// use gpui::{
|
||||
// actions,
|
||||
// elements::{ChildView, Label},
|
||||
// geometry::vector::Vector2F,
|
||||
// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View,
|
||||
// ViewContext, ViewHandle,
|
||||
// };
|
||||
// use project::Project;
|
||||
// use smallvec::SmallVec;
|
||||
// use std::{
|
||||
// any::{Any, TypeId},
|
||||
// sync::Arc,
|
||||
// };
|
||||
// use util::ResultExt;
|
||||
// use workspace::{
|
||||
// item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
// register_followable_item,
|
||||
// searchable::SearchableItemHandle,
|
||||
// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
// };
|
||||
use anyhow::Result;
|
||||
use call::report_call_event_for_channel;
|
||||
use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore};
|
||||
use client::{
|
||||
proto::{self, PeerId},
|
||||
Collaborator, ParticipantIndex,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use editor::{CollaborationHub, Editor, EditorEvent};
|
||||
use gpui::{
|
||||
actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView,
|
||||
IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
};
|
||||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
sync::Arc,
|
||||
};
|
||||
use ui::Label;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle},
|
||||
register_followable_item,
|
||||
searchable::SearchableItemHandle,
|
||||
ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
// actions!(channel_view, [Deploy]);
|
||||
actions!(Deploy);
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// register_followable_item::<ChannelView>(cx)
|
||||
// }
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_followable_item::<ChannelView>(cx)
|
||||
}
|
||||
|
||||
// pub struct ChannelView {
|
||||
// pub editor: ViewHandle<Editor>,
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// remote_id: Option<ViewId>,
|
||||
// _editor_event_subscription: Subscription,
|
||||
// }
|
||||
pub struct ChannelView {
|
||||
pub editor: View<Editor>,
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
remote_id: Option<ViewId>,
|
||||
_editor_event_subscription: Subscription,
|
||||
}
|
||||
|
||||
// impl ChannelView {
|
||||
// pub fn open(
|
||||
// channel_id: ChannelId,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let pane = workspace.read(cx).active_pane().clone();
|
||||
// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_view = channel_view.await?;
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// report_call_event_for_channel(
|
||||
// "open channel notes",
|
||||
// channel_id,
|
||||
// &workspace.read(cx).app_state().client,
|
||||
// cx,
|
||||
// );
|
||||
// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
// });
|
||||
// anyhow::Ok(channel_view)
|
||||
// })
|
||||
// }
|
||||
impl ChannelView {
|
||||
pub fn open(
|
||||
channel_id: ChannelId,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let pane = workspace.read(cx).active_pane().clone();
|
||||
let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx);
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_view = channel_view.await?;
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
report_call_event_for_channel(
|
||||
"open channel notes",
|
||||
channel_id,
|
||||
&workspace.read(cx).app_state().client,
|
||||
cx,
|
||||
);
|
||||
pane.add_item(Box::new(channel_view.clone()), true, true, None, cx);
|
||||
})?;
|
||||
anyhow::Ok(channel_view)
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn open_in_pane(
|
||||
// channel_id: ChannelId,
|
||||
// pane: ViewHandle<Pane>,
|
||||
// workspace: ViewHandle<Workspace>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Task<Result<ViewHandle<Self>>> {
|
||||
// let workspace = workspace.read(cx);
|
||||
// let project = workspace.project().to_owned();
|
||||
// let channel_store = ChannelStore::global(cx);
|
||||
// let language_registry = workspace.app_state().languages.clone();
|
||||
// let markdown = language_registry.language_for_name("Markdown");
|
||||
// let channel_buffer =
|
||||
// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
pub fn open_in_pane(
|
||||
channel_id: ChannelId,
|
||||
pane: View<Pane>,
|
||||
workspace: View<Workspace>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<View<Self>>> {
|
||||
let workspace = workspace.read(cx);
|
||||
let project = workspace.project().to_owned();
|
||||
let channel_store = ChannelStore::global(cx);
|
||||
let language_registry = workspace.app_state().languages.clone();
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
let channel_buffer =
|
||||
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx));
|
||||
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// let channel_buffer = channel_buffer.await?;
|
||||
// let markdown = markdown.await.log_err();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let channel_buffer = channel_buffer.await?;
|
||||
let markdown = markdown.await.log_err();
|
||||
|
||||
// channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
// buffer.buffer().update(cx, |buffer, cx| {
|
||||
// buffer.set_language_registry(language_registry);
|
||||
// if let Some(markdown) = markdown {
|
||||
// buffer.set_language(Some(markdown), cx);
|
||||
// }
|
||||
// })
|
||||
// });
|
||||
channel_buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_language_registry(language_registry);
|
||||
if let Some(markdown) = markdown {
|
||||
buffer.set_language(Some(markdown), cx);
|
||||
}
|
||||
})
|
||||
})?;
|
||||
|
||||
// pane.update(&mut cx, |pane, cx| {
|
||||
// let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
let buffer_id = channel_buffer.read(cx).remote_id(cx);
|
||||
|
||||
// let existing_view = pane
|
||||
// .items_of_type::<Self>()
|
||||
// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
let existing_view = pane
|
||||
.items_of_type::<Self>()
|
||||
.find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id);
|
||||
|
||||
// // If this channel buffer is already open in this pane, just return it.
|
||||
// if let Some(existing_view) = existing_view.clone() {
|
||||
// if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
// return existing_view;
|
||||
// }
|
||||
// }
|
||||
// If this channel buffer is already open in this pane, just return it.
|
||||
if let Some(existing_view) = existing_view.clone() {
|
||||
if existing_view.read(cx).channel_buffer == channel_buffer {
|
||||
return existing_view;
|
||||
}
|
||||
}
|
||||
|
||||
// let view = cx.add_view(|cx| {
|
||||
// let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
// this.acknowledge_buffer_version(cx);
|
||||
// this
|
||||
// });
|
||||
let view = cx.build_view(|cx| {
|
||||
let mut this = Self::new(project, channel_store, channel_buffer, cx);
|
||||
this.acknowledge_buffer_version(cx);
|
||||
this
|
||||
});
|
||||
|
||||
// // If the pane contained a disconnected view for this channel buffer,
|
||||
// // replace that.
|
||||
// if let Some(existing_item) = existing_view {
|
||||
// if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx)
|
||||
// .detach();
|
||||
// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
// }
|
||||
// }
|
||||
// If the pane contained a disconnected view for this channel buffer,
|
||||
// replace that.
|
||||
if let Some(existing_item) = existing_view {
|
||||
if let Some(ix) = pane.index_for_item(&existing_item) {
|
||||
pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx)
|
||||
.detach();
|
||||
pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx);
|
||||
}
|
||||
}
|
||||
|
||||
// view
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("pane was dropped"))
|
||||
// })
|
||||
// }
|
||||
view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn new(
|
||||
// project: ModelHandle<Project>,
|
||||
// channel_store: ModelHandle<ChannelStore>,
|
||||
// channel_buffer: ModelHandle<ChannelBuffer>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> Self {
|
||||
// let buffer = channel_buffer.read(cx).buffer();
|
||||
// let editor = cx.add_view(|cx| {
|
||||
// let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
// channel_buffer.clone(),
|
||||
// )));
|
||||
// editor.set_read_only(
|
||||
// !channel_buffer
|
||||
// .read(cx)
|
||||
// .channel(cx)
|
||||
// .is_some_and(|c| c.can_edit_notes()),
|
||||
// );
|
||||
// editor
|
||||
// });
|
||||
// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone()));
|
||||
pub fn new(
|
||||
project: Model<Project>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_buffer: Model<ChannelBuffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let buffer = channel_buffer.read(cx).buffer();
|
||||
let editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, None, cx);
|
||||
editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub(
|
||||
channel_buffer.clone(),
|
||||
)));
|
||||
editor.set_read_only(
|
||||
!channel_buffer
|
||||
.read(cx)
|
||||
.channel(cx)
|
||||
.is_some_and(|c| c.can_edit_notes()),
|
||||
);
|
||||
editor
|
||||
});
|
||||
let _editor_event_subscription =
|
||||
cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone()));
|
||||
|
||||
// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
// .detach();
|
||||
cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event)
|
||||
.detach();
|
||||
|
||||
// Self {
|
||||
// editor,
|
||||
// project,
|
||||
// channel_store,
|
||||
// channel_buffer,
|
||||
// remote_id: None,
|
||||
// _editor_event_subscription,
|
||||
// }
|
||||
// }
|
||||
Self {
|
||||
editor,
|
||||
project,
|
||||
channel_store,
|
||||
channel_buffer,
|
||||
remote_id: None,
|
||||
_editor_event_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
// self.channel_buffer.read(cx).channel(cx)
|
||||
// }
|
||||
pub fn channel(&self, cx: &AppContext) -> Option<Arc<Channel>> {
|
||||
self.channel_buffer.read(cx).channel(cx)
|
||||
}
|
||||
|
||||
// fn handle_channel_buffer_event(
|
||||
// &mut self,
|
||||
// _: ModelHandle<ChannelBuffer>,
|
||||
// event: &ChannelBufferEvent,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) {
|
||||
// match event {
|
||||
// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(true);
|
||||
// cx.notify();
|
||||
// }),
|
||||
// ChannelBufferEvent::ChannelChanged => {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
// cx.emit(editor::Event::TitleChanged);
|
||||
// cx.notify()
|
||||
// });
|
||||
// }
|
||||
// ChannelBufferEvent::BufferEdited => {
|
||||
// if cx.is_self_focused() || self.editor.is_focused(cx) {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// } else {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.notes_changed(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
// }
|
||||
// }
|
||||
fn handle_channel_buffer_event(
|
||||
&mut self,
|
||||
_: Model<ChannelBuffer>,
|
||||
event: &ChannelBufferEvent,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(true);
|
||||
cx.notify();
|
||||
}),
|
||||
ChannelBufferEvent::ChannelChanged => {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes()));
|
||||
cx.emit(editor::EditorEvent::TitleChanged);
|
||||
cx.notify()
|
||||
});
|
||||
}
|
||||
ChannelBufferEvent::BufferEdited => {
|
||||
if self.editor.read(cx).is_focused(cx) {
|
||||
self.acknowledge_buffer_version(cx);
|
||||
} else {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.notes_changed(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
ChannelBufferEvent::CollaboratorsChanged => {}
|
||||
}
|
||||
}
|
||||
|
||||
// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) {
|
||||
// self.channel_store.update(cx, |store, cx| {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// store.acknowledge_notes_version(
|
||||
// channel_buffer.channel_id,
|
||||
// channel_buffer.epoch(),
|
||||
// &channel_buffer.buffer().read(cx).version(),
|
||||
// cx,
|
||||
// )
|
||||
// });
|
||||
// self.channel_buffer.update(cx, |buffer, cx| {
|
||||
// buffer.acknowledge_buffer_version(cx);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<ChannelView>) {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
store.acknowledge_notes_version(
|
||||
channel_buffer.channel_id,
|
||||
channel_buffer.epoch(),
|
||||
&channel_buffer.buffer().read(cx).version(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
self.channel_buffer.update(cx, |buffer, cx| {
|
||||
buffer.acknowledge_buffer_version(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// impl Entity for ChannelView {
|
||||
// type Event = editor::Event;
|
||||
// }
|
||||
impl EventEmitter<EditorEvent> for ChannelView {}
|
||||
|
||||
// impl View for ChannelView {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "ChannelView"
|
||||
// }
|
||||
impl Render for ChannelView {
|
||||
type Element = AnyView;
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// ChildView::new(self.editor.as_any(), cx).into_any()
|
||||
// }
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.editor.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// if cx.is_self_focused() {
|
||||
// self.acknowledge_buffer_version(cx);
|
||||
// cx.focus(self.editor.as_any())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
impl FocusableView for ChannelView {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
// impl Item for ChannelView {
|
||||
// fn act_as_type<'a>(
|
||||
// &'a self,
|
||||
// type_id: TypeId,
|
||||
// self_handle: &'a ViewHandle<Self>,
|
||||
// _: &'a AppContext,
|
||||
// ) -> Option<&'a AnyViewHandle> {
|
||||
// if type_id == TypeId::of::<Self>() {
|
||||
// Some(self_handle)
|
||||
// } else if type_id == TypeId::of::<Editor>() {
|
||||
// Some(&self.editor)
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
impl Item for ChannelView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
// fn tab_content<V: 'static>(
|
||||
// &self,
|
||||
// _: Option<usize>,
|
||||
// style: &theme::Tab,
|
||||
// cx: &gpui::AppContext,
|
||||
// ) -> AnyElement<V> {
|
||||
// let label = if let Some(channel) = self.channel(cx) {
|
||||
// match (
|
||||
// channel.can_edit_notes(),
|
||||
// self.channel_buffer.read(cx).is_connected(),
|
||||
// ) {
|
||||
// (true, true) => format!("#{}", channel.name),
|
||||
// (false, true) => format!("#{} (read-only)", channel.name),
|
||||
// (_, false) => format!("#{} (disconnected)", channel.name),
|
||||
// }
|
||||
// } else {
|
||||
// format!("channel notes (disconnected)")
|
||||
// };
|
||||
// Label::new(label, style.label.to_owned()).into_any()
|
||||
// }
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a View<Self>,
|
||||
_: &'a AppContext,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self> {
|
||||
// Some(Self::new(
|
||||
// self.project.clone(),
|
||||
// self.channel_store.clone(),
|
||||
// self.channel_buffer.clone(),
|
||||
// cx,
|
||||
// ))
|
||||
// }
|
||||
fn tab_content(&self, _: Option<usize>, cx: &WindowContext) -> AnyElement {
|
||||
let label = if let Some(channel) = self.channel(cx) {
|
||||
match (
|
||||
channel.can_edit_notes(),
|
||||
self.channel_buffer.read(cx).is_connected(),
|
||||
) {
|
||||
(true, true) => format!("#{}", channel.name),
|
||||
(false, true) => format!("#{} (read-only)", channel.name),
|
||||
(_, false) => format!("#{} (disconnected)", channel.name),
|
||||
}
|
||||
} else {
|
||||
format!("channel notes (disconnected)")
|
||||
};
|
||||
Label::new(label).into_any_element()
|
||||
}
|
||||
|
||||
// fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<View<Self>> {
|
||||
Some(cx.build_view(|cx| {
|
||||
Self::new(
|
||||
self.project.clone(),
|
||||
self.channel_store.clone(),
|
||||
self.channel_buffer.clone(),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
// }
|
||||
fn is_singleton(&self, _cx: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
// }
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
// self.editor
|
||||
// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
// }
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::deactivated(editor, cx))
|
||||
}
|
||||
|
||||
// fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
// Some(Box::new(self.editor.clone()))
|
||||
// }
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| Item::set_nav_history(editor, history, cx))
|
||||
}
|
||||
|
||||
// fn show_toolbar(&self) -> bool {
|
||||
// true
|
||||
// }
|
||||
fn as_searchable(&self, _: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
|
||||
// self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
// }
|
||||
fn show_toolbar(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
// editor::Editor::to_item_events(event)
|
||||
// }
|
||||
// }
|
||||
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
|
||||
self.editor.read(cx).pixel_position_of_cursor(cx)
|
||||
}
|
||||
|
||||
// impl FollowableItem for ChannelView {
|
||||
// fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
// self.remote_id
|
||||
// }
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
}
|
||||
|
||||
// fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
|
||||
// let channel_buffer = self.channel_buffer.read(cx);
|
||||
// if !channel_buffer.is_connected() {
|
||||
// return None;
|
||||
// }
|
||||
impl FollowableItem for ChannelView {
|
||||
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
// Some(proto::view::Variant::ChannelView(
|
||||
// proto::view::ChannelView {
|
||||
// channel_id: channel_buffer.channel_id,
|
||||
// editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
// self.editor.read(cx).to_state_proto(cx)
|
||||
// {
|
||||
// Some(proto)
|
||||
// } else {
|
||||
// None
|
||||
// },
|
||||
// },
|
||||
// ))
|
||||
// }
|
||||
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
|
||||
let channel_buffer = self.channel_buffer.read(cx);
|
||||
if !channel_buffer.is_connected() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// fn from_state_proto(
|
||||
// pane: ViewHandle<workspace::Pane>,
|
||||
// workspace: ViewHandle<workspace::Workspace>,
|
||||
// remote_id: workspace::ViewId,
|
||||
// state: &mut Option<proto::view::Variant>,
|
||||
// cx: &mut AppContext,
|
||||
// ) -> Option<gpui::Task<anyhow::Result<ViewHandle<Self>>>> {
|
||||
// let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
// return None;
|
||||
// };
|
||||
// let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
// unreachable!()
|
||||
// };
|
||||
Some(proto::view::Variant::ChannelView(
|
||||
proto::view::ChannelView {
|
||||
channel_id: channel_buffer.channel_id,
|
||||
editor: if let Some(proto::view::Variant::Editor(proto)) =
|
||||
self.editor.read(cx).to_state_proto(cx)
|
||||
{
|
||||
Some(proto)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
fn from_state_proto(
|
||||
pane: View<workspace::Pane>,
|
||||
workspace: View<workspace::Workspace>,
|
||||
remote_id: workspace::ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<gpui::Task<anyhow::Result<View<Self>>>> {
|
||||
let Some(proto::view::Variant::ChannelView(_)) = state else {
|
||||
return None;
|
||||
};
|
||||
let Some(proto::view::Variant::ChannelView(state)) = state.take() else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
// Some(cx.spawn(|mut cx| async move {
|
||||
// let this = open.await?;
|
||||
let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx);
|
||||
|
||||
// let task = this
|
||||
// .update(&mut cx, |this, cx| {
|
||||
// this.remote_id = Some(remote_id);
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let this = open.await?;
|
||||
|
||||
// if let Some(state) = state.editor {
|
||||
// Some(this.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(
|
||||
// &this.project,
|
||||
// proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
// selections: state.selections,
|
||||
// pending_selection: state.pending_selection,
|
||||
// scroll_top_anchor: state.scroll_top_anchor,
|
||||
// scroll_x: state.scroll_x,
|
||||
// scroll_y: state.scroll_y,
|
||||
// ..Default::default()
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// }))
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// })
|
||||
// .ok_or_else(|| anyhow!("window was closed"))?;
|
||||
let task = this.update(&mut cx, |this, cx| {
|
||||
this.remote_id = Some(remote_id);
|
||||
|
||||
// if let Some(task) = task {
|
||||
// task.await?;
|
||||
// }
|
||||
if let Some(state) = state.editor {
|
||||
Some(this.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(
|
||||
&this.project,
|
||||
proto::update_view::Variant::Editor(proto::update_view::Editor {
|
||||
selections: state.selections,
|
||||
pending_selection: state.pending_selection,
|
||||
scroll_top_anchor: state.scroll_top_anchor,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
|
||||
// Ok(this)
|
||||
// }))
|
||||
// }
|
||||
if let Some(task) = task {
|
||||
task.await?;
|
||||
}
|
||||
|
||||
// fn add_event_to_update_proto(
|
||||
// &self,
|
||||
// event: &Self::Event,
|
||||
// update: &mut Option<proto::update_view::Variant>,
|
||||
// cx: &AppContext,
|
||||
// ) -> bool {
|
||||
// self.editor
|
||||
// .read(cx)
|
||||
// .add_event_to_update_proto(event, update, cx)
|
||||
// }
|
||||
Ok(this)
|
||||
}))
|
||||
}
|
||||
|
||||
// fn apply_update_proto(
|
||||
// &mut self,
|
||||
// project: &ModelHandle<Project>,
|
||||
// message: proto::update_view::Variant,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> gpui::Task<anyhow::Result<()>> {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.apply_update_proto(project, message, cx)
|
||||
// })
|
||||
// }
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
event: &EditorEvent,
|
||||
update: &mut Option<proto::update_view::Variant>,
|
||||
cx: &WindowContext,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.read(cx)
|
||||
.add_event_to_update_proto(event, update, cx)
|
||||
}
|
||||
|
||||
// fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
// self.editor.update(cx, |editor, cx| {
|
||||
// editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
// })
|
||||
// }
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
project: &Model<Project>,
|
||||
message: proto::update_view::Variant,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> gpui::Task<anyhow::Result<()>> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.apply_update_proto(project, message, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool {
|
||||
// Editor::should_unfollow_on_event(event, cx)
|
||||
// }
|
||||
fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_leader_peer_id(leader_peer_id, cx)
|
||||
})
|
||||
}
|
||||
|
||||
// fn is_project_item(&self, _cx: &AppContext) -> bool {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
fn is_project_item(&self, _cx: &WindowContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
// struct ChannelBufferCollaborationHub(ModelHandle<ChannelBuffer>);
|
||||
fn to_follow_event(event: &Self::Event) -> Option<workspace::item::FollowEvent> {
|
||||
Editor::to_follow_event(event)
|
||||
}
|
||||
}
|
||||
|
||||
// impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
// self.0.read(cx).collaborators()
|
||||
// }
|
||||
struct ChannelBufferCollaborationHub(Model<ChannelBuffer>);
|
||||
|
||||
// fn user_participant_indices<'a>(
|
||||
// &self,
|
||||
// cx: &'a AppContext,
|
||||
// ) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
// self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
// }
|
||||
// }
|
||||
impl CollaborationHub for ChannelBufferCollaborationHub {
|
||||
fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
|
||||
self.0.read(cx).collaborators()
|
||||
}
|
||||
|
||||
fn user_participant_indices<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a HashMap<u64, ParticipantIndex> {
|
||||
self.0.read(cx).user_store().read(cx).participant_indices()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ use client::UserId;
|
||||
use collections::HashMap;
|
||||
use editor::{AnchorRangeExt, Editor};
|
||||
use gpui::{
|
||||
elements::ChildView, AnyElement, AsyncAppContext, Element, Entity, ModelHandle, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AnyView, AsyncWindowContext, FocusableView, Model, Render, SharedString, Task, View,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
|
||||
use lazy_static::lazy_static;
|
||||
use project::search::SearchQuery;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use workspace::item::ItemHandle;
|
||||
|
||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||
|
||||
@@ -19,8 +20,8 @@ lazy_static! {
|
||||
}
|
||||
|
||||
pub struct MessageEditor {
|
||||
pub editor: ViewHandle<Editor>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
pub editor: View<Editor>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
users: HashMap<String, UserId>,
|
||||
mentions: Vec<UserId>,
|
||||
mentions_task: Option<Task<()>>,
|
||||
@@ -30,8 +31,8 @@ pub struct MessageEditor {
|
||||
impl MessageEditor {
|
||||
pub fn new(
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
editor: ViewHandle<Editor>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
editor: View<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -48,15 +49,13 @@ impl MessageEditor {
|
||||
cx.subscribe(&buffer, Self::on_buffer_event).detach();
|
||||
|
||||
let markdown = language_registry.language_for_name("Markdown");
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
});
|
||||
anyhow::Ok(())
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let markdown = markdown.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(Some(markdown), cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Self {
|
||||
editor,
|
||||
@@ -71,7 +70,7 @@ impl MessageEditor {
|
||||
pub fn set_channel(
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
channel_name: Option<String>,
|
||||
channel_name: Option<SharedString>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
@@ -132,26 +131,28 @@ impl MessageEditor {
|
||||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
buffer: Model<Buffer>,
|
||||
event: &language::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let language::Event::Reparsed | language::Event::Edited = event {
|
||||
let buffer = buffer.read(cx).snapshot();
|
||||
self.mentions_task = Some(cx.spawn(|this, cx| async move {
|
||||
cx.background().timer(MENTIONS_DEBOUNCE_INTERVAL).await;
|
||||
cx.background_executor()
|
||||
.timer(MENTIONS_DEBOUNCE_INTERVAL)
|
||||
.await;
|
||||
Self::find_mentions(this, buffer, cx).await;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_mentions(
|
||||
this: WeakViewHandle<MessageEditor>,
|
||||
this: WeakView<MessageEditor>,
|
||||
buffer: BufferSnapshot,
|
||||
mut cx: AsyncAppContext,
|
||||
mut cx: AsyncWindowContext,
|
||||
) {
|
||||
let (buffer, ranges) = cx
|
||||
.background()
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
let ranges = MENTIONS_SEARCH.search(&buffer, None).await;
|
||||
(buffer, ranges)
|
||||
@@ -180,11 +181,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
editor.clear_highlights::<Self>(cx);
|
||||
editor.highlight_text::<Self>(
|
||||
anchor_ranges,
|
||||
theme::current(cx).chat_panel.rich_text.mention_highlight,
|
||||
cx,
|
||||
)
|
||||
editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx)
|
||||
});
|
||||
|
||||
this.mentions = mentioned_user_ids;
|
||||
@@ -192,21 +189,17 @@ impl MessageEditor {
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for MessageEditor {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for MessageEditor {
|
||||
fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement<Self> {
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
pub(crate) fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.editor);
|
||||
}
|
||||
impl Render for MessageEditor {
|
||||
type Element = AnyView;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
self.editor.to_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +207,7 @@ impl View for MessageEditor {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, User, UserStore};
|
||||
use gpui::{TestAppContext, WindowHandle};
|
||||
use gpui::{Context as _, TestAppContext, VisualContext as _};
|
||||
use language::{Language, LanguageConfig};
|
||||
use rpc::proto;
|
||||
use settings::SettingsStore;
|
||||
@@ -222,8 +215,17 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_message_editor(cx: &mut TestAppContext) {
|
||||
let editor = init_test(cx);
|
||||
let editor = editor.root(cx);
|
||||
let language_registry = init_test(cx);
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|cx| {
|
||||
MessageEditor::new(
|
||||
language_registry,
|
||||
ChannelStore::global(cx),
|
||||
cx.build_view(|cx| Editor::auto_height(4, cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_members(
|
||||
@@ -255,7 +257,7 @@ mod tests {
|
||||
});
|
||||
});
|
||||
|
||||
cx.foreground().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||
cx.executor().advance_clock(MENTIONS_DEBOUNCE_INTERVAL);
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
let (text, ranges) = marked_text_ranges("Hello, «@a-b»! Have you met «@C_D»?", false);
|
||||
@@ -269,15 +271,14 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> WindowHandle<MessageEditor> {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<LanguageRegistry> {
|
||||
cx.update(|cx| {
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(http.clone(), cx);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
client::init(&client, cx);
|
||||
@@ -292,16 +293,6 @@ mod tests {
|
||||
},
|
||||
Some(tree_sitter_markdown::language()),
|
||||
)));
|
||||
|
||||
let editor = cx.add_window(|cx| {
|
||||
MessageEditor::new(
|
||||
language_registry,
|
||||
ChannelStore::global(cx),
|
||||
cx.add_view(|cx| Editor::auto_height(4, None, cx)),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
editor
|
||||
language_registry
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,58 +3,55 @@ use client::{
|
||||
proto::{self, ChannelRole, ChannelVisibility},
|
||||
User, UserId, UserStore,
|
||||
};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
||||
ViewHandle,
|
||||
actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
|
||||
FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
|
||||
WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use ui::prelude::*;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
use workspace::ModalView;
|
||||
|
||||
actions!(
|
||||
channel_modal,
|
||||
[
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
]
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ChannelModalDelegate>::init(cx);
|
||||
cx.add_action(ChannelModal::toggle_mode);
|
||||
cx.add_action(ChannelModal::toggle_member_admin);
|
||||
cx.add_action(ChannelModal::remove_member);
|
||||
cx.add_action(ChannelModal::dismiss);
|
||||
}
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// Picker::<ChannelModalDelegate>::init(cx);
|
||||
// cx.add_action(ChannelModal::toggle_mode);
|
||||
// cx.add_action(ChannelModal::toggle_member_admin);
|
||||
// cx.add_action(ChannelModal::remove_member);
|
||||
// cx.add_action(ChannelModal::dismiss);
|
||||
// }
|
||||
|
||||
pub struct ChannelModal {
|
||||
picker: ViewHandle<Picker<ChannelModalDelegate>>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
picker: View<Picker<ChannelModalDelegate>>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ChannelModal {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
mode: Mode,
|
||||
members: Vec<ChannelMembership>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
let picker = cx.add_view(|cx| {
|
||||
let channel_modal = cx.view().downgrade();
|
||||
let picker = cx.build_view(|cx| {
|
||||
Picker::new(
|
||||
ChannelModalDelegate {
|
||||
channel_modal,
|
||||
matching_users: Vec::new(),
|
||||
matching_member_indices: Vec::new(),
|
||||
selected_index: 0,
|
||||
@@ -64,20 +61,17 @@ impl ChannelModal {
|
||||
match_candidates: Vec::new(),
|
||||
members,
|
||||
mode,
|
||||
context_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
// context_menu: cx.add_view(|cx| {
|
||||
// let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
// menu.set_position_mode(OverlayPositionMode::Local);
|
||||
// menu
|
||||
// }),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
let has_focus = picker.read(cx).has_focus();
|
||||
let has_focus = picker.focus_handle(cx).contains_focused(cx);
|
||||
|
||||
Self {
|
||||
picker,
|
||||
@@ -88,7 +82,7 @@ impl ChannelModal {
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||
let mode = match self.picker.read(cx).delegate().mode {
|
||||
let mode = match self.picker.read(cx).delegate.mode {
|
||||
Mode::ManageMembers => Mode::InviteMembers,
|
||||
Mode::InviteMembers => Mode::ManageMembers,
|
||||
};
|
||||
@@ -103,20 +97,20 @@ impl ChannelModal {
|
||||
let mut members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||
.update(cx, |picker, _| picker.delegate.members = members);
|
||||
})?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
@@ -131,203 +125,195 @@ impl ChannelModal {
|
||||
|
||||
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().toggle_selected_member_admin(cx);
|
||||
picker.delegate.toggle_selected_member_admin(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().remove_selected_member(cx);
|
||||
picker.delegate.remove_selected_member(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChannelModal {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for ChannelModal {}
|
||||
impl ModalView for ChannelModal {}
|
||||
|
||||
impl View for ChannelModal {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChannelModal"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
let mode = self.picker.read(cx).delegate().mode;
|
||||
let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||
return Empty::new().into_any();
|
||||
};
|
||||
|
||||
enum InviteMembers {}
|
||||
enum ManageMembers {}
|
||||
|
||||
fn render_mode_button<T: 'static>(
|
||||
mode: Mode,
|
||||
text: &'static str,
|
||||
current_mode: Mode,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
let active = mode == current_mode;
|
||||
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
let contained_text = theme.tab_button.style_for(active, state);
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if !active {
|
||||
this.set_mode(mode, cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_visibility(
|
||||
channel_id: ChannelId,
|
||||
visibility: ChannelVisibility,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
enum TogglePublic {}
|
||||
|
||||
if visibility == ChannelVisibility::Members {
|
||||
return Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
let style = theme.visibility_toggle.style_for(state);
|
||||
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.channel_store
|
||||
.update(cx, |channel_store, cx| {
|
||||
channel_store.set_channel_visibility(
|
||||
channel_id,
|
||||
ChannelVisibility::Members,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.with_spacing(14.0)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
let style = theme.channel_link.style_for(state);
|
||||
Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(channel) =
|
||||
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
{
|
||||
let item = ClipboardItem::new(channel.link());
|
||||
cx.write_to_clipboard(item);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
.with_child(Flex::row().with_children([
|
||||
render_mode_button::<InviteMembers>(
|
||||
Mode::InviteMembers,
|
||||
"Invite members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
render_mode_button::<ManageMembers>(
|
||||
Mode::ManageMembers,
|
||||
"Manage members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
impl FocusableView for ChannelModal {
|
||||
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ChannelModal {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
impl Render for ChannelModal {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
// let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
// let mode = self.picker.read(cx).delegate().mode;
|
||||
// let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
|
||||
// return Empty::new().into_any();
|
||||
// };
|
||||
|
||||
// enum InviteMembers {}
|
||||
// enum ManageMembers {}
|
||||
|
||||
// fn render_mode_button<T: 'static>(
|
||||
// mode: Mode,
|
||||
// text: &'static str,
|
||||
// current_mode: Mode,
|
||||
// theme: &theme::TabbedModal,
|
||||
// cx: &mut ViewContext<ChannelModal>,
|
||||
// ) -> AnyElement<ChannelModal> {
|
||||
// let active = mode == current_mode;
|
||||
// MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
// let contained_text = theme.tab_button.style_for(active, state);
|
||||
// Label::new(text, contained_text.text.clone())
|
||||
// .contained()
|
||||
// .with_style(contained_text.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// if !active {
|
||||
// this.set_mode(mode, cx);
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand)
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_visibility(
|
||||
// channel_id: ChannelId,
|
||||
// visibility: ChannelVisibility,
|
||||
// theme: &theme::TabbedModal,
|
||||
// cx: &mut ViewContext<ChannelModal>,
|
||||
// ) -> AnyElement<ChannelModal> {
|
||||
// enum TogglePublic {}
|
||||
|
||||
// if visibility == ChannelVisibility::Members {
|
||||
// return Flex::row()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
// let style = theme.visibility_toggle.style_for(state);
|
||||
// Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.channel_store
|
||||
// .update(cx, |channel_store, cx| {
|
||||
// channel_store.set_channel_visibility(
|
||||
// channel_id,
|
||||
// ChannelVisibility::Public,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .into_any();
|
||||
// }
|
||||
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||
// let style = theme.visibility_toggle.style_for(state);
|
||||
// Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// this.channel_store
|
||||
// .update(cx, |channel_store, cx| {
|
||||
// channel_store.set_channel_visibility(
|
||||
// channel_id,
|
||||
// ChannelVisibility::Members,
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .detach_and_log_err(cx);
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .with_spacing(14.0)
|
||||
// .with_child(
|
||||
// MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||
// let style = theme.channel_link.style_for(state);
|
||||
// Label::new(format!("{}", "copy link"), style.text.clone())
|
||||
// .contained()
|
||||
// .with_style(style.container.clone())
|
||||
// })
|
||||
// .on_click(MouseButton::Left, move |_, this, cx| {
|
||||
// if let Some(channel) =
|
||||
// this.channel_store.read(cx).channel_for_id(channel_id)
|
||||
// {
|
||||
// let item = ClipboardItem::new(channel.link());
|
||||
// cx.write_to_clipboard(item);
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::PointingHand),
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.title.container.clone()),
|
||||
// )
|
||||
// .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||
// .with_child(Flex::row().with_children([
|
||||
// render_mode_button::<InviteMembers>(
|
||||
// Mode::InviteMembers,
|
||||
// "Invite members",
|
||||
// mode,
|
||||
// theme,
|
||||
// cx,
|
||||
// ),
|
||||
// render_mode_button::<ManageMembers>(
|
||||
// Mode::ManageMembers,
|
||||
// "Manage members",
|
||||
// mode,
|
||||
// theme,
|
||||
// cx,
|
||||
// ),
|
||||
// ]))
|
||||
// .expanded()
|
||||
// .contained()
|
||||
// .with_style(theme.header),
|
||||
// )
|
||||
// .with_child(
|
||||
// ChildView::new(&self.picker, cx)
|
||||
// .contained()
|
||||
// .with_style(theme.body),
|
||||
// )
|
||||
// .constrained()
|
||||
// .with_max_height(theme.max_height)
|
||||
// .with_max_width(theme.max_width)
|
||||
// .contained()
|
||||
// .with_style(theme.modal)
|
||||
// .into_any()
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if cx.is_self_focused() {
|
||||
// cx.focus(&self.picker)
|
||||
// }
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
@@ -337,19 +323,22 @@ pub enum Mode {
|
||||
}
|
||||
|
||||
pub struct ChannelModalDelegate {
|
||||
channel_modal: WeakView<ChannelModal>,
|
||||
matching_users: Vec<Arc<User>>,
|
||||
matching_member_indices: Vec<usize>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
user_store: Model<UserStore>,
|
||||
channel_store: Model<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
selected_index: usize,
|
||||
mode: Mode,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
// context_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ChannelModalDelegate {
|
||||
type ListItem = Div;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
@@ -382,19 +371,19 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background().block(match_strings(
|
||||
let matches = cx.background_executor().block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background().clone(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
let delegate = &mut picker.delegate;
|
||||
delegate.matching_member_indices.clear();
|
||||
delegate
|
||||
.matching_member_indices
|
||||
@@ -412,8 +401,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
async {
|
||||
let users = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_users = users;
|
||||
picker.delegate.matching_users = users;
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
@@ -445,138 +433,142 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
self.channel_modal
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, role) = self.user_at_index(ix).unwrap();
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
None
|
||||
// let full_theme = &theme::current(cx);
|
||||
// let theme = &full_theme.collab_panel.channel_modal;
|
||||
// let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
// let (user, role) = self.user_at_index(ix).unwrap();
|
||||
// let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
// let style = tabbed_modal
|
||||
// .picker
|
||||
// .item
|
||||
// .in_state(selected)
|
||||
// .style_for(mouse_state);
|
||||
|
||||
let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
// let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
|
||||
let mut result = Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children({
|
||||
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
|| {
|
||||
Label::new("Invited", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
Some(
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
Some(
|
||||
Label::new("Guest", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
Svg::new("icons/ellipsis.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Mode::InviteMembers => match request_status {
|
||||
Some(proto::channel_member::Kind::Member) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.invitee_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.button_width)
|
||||
.with_height(theme.invitee_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.invitee_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
},
|
||||
};
|
||||
// let mut result = Flex::row()
|
||||
// .with_children(user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.contact_avatar)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Label::new(user.github_login.clone(), style.label.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.contact_username)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_children({
|
||||
// (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
// || {
|
||||
// Label::new("Invited", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// },
|
||||
// )
|
||||
// })
|
||||
// .with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||
// Some(
|
||||
// Label::new("Admin", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// } else if in_manage && role == Some(ChannelRole::Guest) {
|
||||
// Some(
|
||||
// Label::new("Guest", theme.member_tag.text.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.member_tag.container)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// } else {
|
||||
// None
|
||||
// })
|
||||
// .with_children({
|
||||
// let svg = match self.mode {
|
||||
// Mode::ManageMembers => Some(
|
||||
// Svg::new("icons/ellipsis.svg")
|
||||
// .with_color(theme.member_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.button_width)
|
||||
// .with_height(theme.member_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.member_icon.container),
|
||||
// ),
|
||||
// Mode::InviteMembers => match request_status {
|
||||
// Some(proto::channel_member::Kind::Member) => Some(
|
||||
// Svg::new("icons/check.svg")
|
||||
// .with_color(theme.member_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.member_icon.button_width)
|
||||
// .with_height(theme.member_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.member_icon.container),
|
||||
// ),
|
||||
// Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
// Svg::new("icons/check.svg")
|
||||
// .with_color(theme.invitee_icon.color)
|
||||
// .constrained()
|
||||
// .with_width(theme.invitee_icon.icon_width)
|
||||
// .aligned()
|
||||
// .constrained()
|
||||
// .with_width(theme.invitee_icon.button_width)
|
||||
// .with_height(theme.invitee_icon.button_width)
|
||||
// .contained()
|
||||
// .with_style(theme.invitee_icon.container),
|
||||
// ),
|
||||
// Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
// },
|
||||
// };
|
||||
|
||||
svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
})
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any();
|
||||
// svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
// })
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_height(tabbed_modal.row_height)
|
||||
// .into_any();
|
||||
|
||||
if selected {
|
||||
result = Stack::new()
|
||||
.with_child(result)
|
||||
.with_child(
|
||||
ChildView::new(&self.context_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right(),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
// if selected {
|
||||
// result = Stack::new()
|
||||
// .with_child(result)
|
||||
// .with_child(
|
||||
// ChildView::new(&self.context_menu, cx)
|
||||
// .aligned()
|
||||
// .top()
|
||||
// .right(),
|
||||
// )
|
||||
// .into_any();
|
||||
// }
|
||||
|
||||
result
|
||||
// result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,7 +615,7 @@ impl ChannelModalDelegate {
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.role = new_role;
|
||||
}
|
||||
@@ -644,7 +636,7 @@ impl ChannelModalDelegate {
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
let this = &mut picker.delegate;
|
||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||
this.members.remove(ix);
|
||||
this.matching_member_indices.retain_mut(|member_ix| {
|
||||
@@ -683,7 +675,7 @@ impl ChannelModalDelegate {
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
role: ChannelRole::Member,
|
||||
};
|
||||
let members = &mut this.delegate_mut().members;
|
||||
let members = &mut this.delegate.members;
|
||||
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||
}
|
||||
@@ -695,23 +687,23 @@ impl ChannelModalDelegate {
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if role == ChannelRole::Admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
},
|
||||
ToggleMemberAdmin,
|
||||
),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
// self.context_menu.update(cx, |context_menu, cx| {
|
||||
// context_menu.show(
|
||||
// Default::default(),
|
||||
// AnchorCorner::TopRight,
|
||||
// vec![
|
||||
// ContextMenuItem::action("Remove", RemoveMember),
|
||||
// ContextMenuItem::action(
|
||||
// if role == ChannelRole::Admin {
|
||||
// "Make non-admin"
|
||||
// } else {
|
||||
// "Make admin"
|
||||
// },
|
||||
// ToggleMemberAdmin,
|
||||
// ),
|
||||
// ],
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,35 @@
|
||||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle,
|
||||
FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View,
|
||||
ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
use theme::ActiveTheme as _;
|
||||
use ui::prelude::*;
|
||||
use util::{ResultExt as _, TryFutureExt};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ContactFinderDelegate>::init(cx);
|
||||
cx.add_action(ContactFinder::dismiss)
|
||||
//Picker::<ContactFinderDelegate>::init(cx);
|
||||
//cx.add_action(ContactFinder::dismiss)
|
||||
}
|
||||
|
||||
pub struct ContactFinder {
|
||||
picker: ViewHandle<Picker<ContactFinderDelegate>>,
|
||||
picker: View<Picker<ContactFinderDelegate>>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ContactFinder {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let picker = cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
ContactFinderDelegate {
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let delegate = ContactFinderDelegate {
|
||||
parent: cx.view().downgrade(),
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
};
|
||||
let picker = cx.build_view(|cx| Picker::new(delegate, cx));
|
||||
|
||||
Self {
|
||||
picker,
|
||||
@@ -41,105 +39,73 @@ impl ContactFinder {
|
||||
|
||||
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.set_query(query, cx);
|
||||
// todo!()
|
||||
// picker.set_query(query, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactFinder {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
|
||||
impl View for ContactFinder {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactFinder"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.tabbed_modal;
|
||||
|
||||
fn render_mode_button(
|
||||
text: &'static str,
|
||||
theme: &theme::TabbedModal,
|
||||
_cx: &mut ViewContext<ContactFinder>,
|
||||
) -> AnyElement<ContactFinder> {
|
||||
let contained_text = &theme.tab_button.active_state().default;
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
.into_any()
|
||||
impl Render for ContactFinder {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render_mode_button(text: &'static str) -> AnyElement {
|
||||
Label::new(text).into_any_element()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new("Contacts", theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(Flex::row().with_children([render_mode_button(
|
||||
"Invite new contacts",
|
||||
&theme,
|
||||
cx,
|
||||
)]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
v_stack()
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new("Contacts"))
|
||||
.child(h_stack().children([render_mode_button("Invite new contacts")]))
|
||||
.bg(cx.theme().colors().element_background),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
.child(self.picker.clone())
|
||||
.w(rems(34.))
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
// fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// self.has_focus = true;
|
||||
// if cx.is_self_focused() {
|
||||
// cx.focus(&self.picker)
|
||||
// }
|
||||
// }
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
// fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
// self.has_focus = false;
|
||||
// }
|
||||
|
||||
type Element = Div;
|
||||
}
|
||||
|
||||
impl Modal for ContactFinder {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
// impl Modal for ContactFinder {
|
||||
// fn has_focus(&self) -> bool {
|
||||
// self.has_focus
|
||||
// }
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
// fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
// match event {
|
||||
// PickerEvent::Dismiss => true,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
parent: WeakView<ContactFinder>,
|
||||
potential_contacts: Arc<[Arc<User>]>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
user_store: Model<UserStore>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContactFinderDelegate {
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
impl EventEmitter<DismissEvent> for ContactFinder {}
|
||||
impl ModalView for ContactFinder {}
|
||||
|
||||
impl FocusableView for ContactFinder {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContactFinderDelegate {
|
||||
type ListItem = Div;
|
||||
fn match_count(&self) -> usize {
|
||||
self.potential_contacts.len()
|
||||
}
|
||||
@@ -152,6 +118,10 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let search_users = self
|
||||
.user_store
|
||||
@@ -161,7 +131,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
async {
|
||||
let potential_contacts = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.delegate_mut().potential_contacts = potential_contacts.into();
|
||||
picker.delegate.potential_contacts = potential_contacts.into();
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
@@ -191,19 +161,18 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
//cx.emit(PickerEvent::Dismiss);
|
||||
self.parent
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.contact_finder;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let user = &self.potential_contacts[ix];
|
||||
let request_status = self.user_store.read(cx).contact_request_status(user);
|
||||
|
||||
@@ -214,48 +183,46 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
||||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||
&theme.disabled_contact_button
|
||||
} else {
|
||||
&theme.contact_button
|
||||
};
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children(icon_path.map(|icon_path| {
|
||||
Svg::new(icon_path)
|
||||
.with_color(button_style.color)
|
||||
.constrained()
|
||||
.with_width(button_style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(button_style.container)
|
||||
.constrained()
|
||||
.with_width(button_style.button_width)
|
||||
.with_height(button_style.button_width)
|
||||
.aligned()
|
||||
.flex_float()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any()
|
||||
Some(
|
||||
div()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.children(user.avatar.clone().map(|avatar| img(avatar)))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.children(icon_path.map(|icon_path| svg().path(icon_path))),
|
||||
)
|
||||
// Flex::row()
|
||||
// .with_children(user.avatar.clone().map(|avatar| {
|
||||
// Image::from_data(avatar)
|
||||
// .with_style(theme.contact_avatar)
|
||||
// .aligned()
|
||||
// .left()
|
||||
// }))
|
||||
// .with_child(
|
||||
// Label::new(user.github_login.clone(), style.label.clone())
|
||||
// .contained()
|
||||
// .with_style(theme.contact_username)
|
||||
// .aligned()
|
||||
// .left(),
|
||||
// )
|
||||
// .with_children(icon_path.map(|icon_path| {
|
||||
// Svg::new(icon_path)
|
||||
// .with_color(button_style.color)
|
||||
// .constrained()
|
||||
// .with_width(button_style.icon_width)
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(button_style.container)
|
||||
// .constrained()
|
||||
// .with_width(button_style.button_width)
|
||||
// .with_height(button_style.button_width)
|
||||
// .aligned()
|
||||
// .flex_float()
|
||||
// }))
|
||||
// .contained()
|
||||
// .with_style(style.container)
|
||||
// .constrained()
|
||||
// .with_height(tabbed_modal.row_height)
|
||||
// .into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,23 +7,26 @@ pub mod notification_panel;
|
||||
pub mod notifications;
|
||||
mod panel_settings;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
|
||||
use call::{report_call_event_for_room, ActiveCall, Room};
|
||||
pub use collab_panel::CollabPanel;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::AppContext;
|
||||
use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
|
||||
use gpui::{
|
||||
actions, point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, Task, WindowBounds,
|
||||
WindowKind, WindowOptions,
|
||||
};
|
||||
pub use panel_settings::{
|
||||
ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings,
|
||||
};
|
||||
use settings::Settings;
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
// actions!(
|
||||
// collab,
|
||||
// [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
|
||||
// );
|
||||
actions!(ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall);
|
||||
|
||||
pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
CollaborationPanelSettings::register(cx);
|
||||
ChatPanelSettings::register(cx);
|
||||
NotificationPanelSettings::register(cx);
|
||||
@@ -31,95 +34,101 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
collab_panel::init(cx);
|
||||
// chat_panel::init(cx);
|
||||
// notifications::init(&app_state, cx);
|
||||
channel_view::init(cx);
|
||||
chat_panel::init(cx);
|
||||
notifications::init(&app_state, cx);
|
||||
|
||||
// cx.add_global_action(toggle_screen_sharing);
|
||||
// cx.add_global_action(toggle_mute);
|
||||
// cx.add_global_action(toggle_deafen);
|
||||
}
|
||||
|
||||
// pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
// let call = ActiveCall::global(cx).read(cx);
|
||||
// if let Some(room) = call.room().cloned() {
|
||||
// let client = call.client();
|
||||
// let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
// if room.is_screen_sharing() {
|
||||
// report_call_event_for_room(
|
||||
// "disable screen share",
|
||||
// room.id(),
|
||||
// room.channel_id(),
|
||||
// &client,
|
||||
// cx,
|
||||
// );
|
||||
// Task::ready(room.unshare_screen(cx))
|
||||
// } else {
|
||||
// report_call_event_for_room(
|
||||
// "enable screen share",
|
||||
// room.id(),
|
||||
// room.channel_id(),
|
||||
// &client,
|
||||
// cx,
|
||||
// );
|
||||
// room.share_screen(cx)
|
||||
// }
|
||||
// });
|
||||
// toggle_screen_sharing.detach_and_log_err(cx);
|
||||
// }
|
||||
// }
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
let call = ActiveCall::global(cx).read(cx);
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
report_call_event_for_room(
|
||||
"disable screen share",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
report_call_event_for_room(
|
||||
"enable screen share",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
// let call = ActiveCall::global(cx).read(cx);
|
||||
// if let Some(room) = call.room().cloned() {
|
||||
// let client = call.client();
|
||||
// room.update(cx, |room, cx| {
|
||||
// let operation = if room.is_muted(cx) {
|
||||
// "enable microphone"
|
||||
// } else {
|
||||
// "disable microphone"
|
||||
// };
|
||||
// report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
|
||||
pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
let call = ActiveCall::global(cx).read(cx);
|
||||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
room.update(cx, |room, cx| {
|
||||
let operation = if room.is_muted(cx) {
|
||||
"enable microphone"
|
||||
} else {
|
||||
"disable microphone"
|
||||
};
|
||||
report_call_event_for_room(operation, room.id(), room.channel_id(), &client, cx);
|
||||
|
||||
// room.toggle_mute(cx)
|
||||
// })
|
||||
// .map(|task| task.detach_and_log_err(cx))
|
||||
// .log_err();
|
||||
// }
|
||||
// }
|
||||
room.toggle_mute(cx)
|
||||
})
|
||||
.map(|task| task.detach_and_log_err(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
// if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
// room.update(cx, Room::toggle_deafen)
|
||||
// .map(|task| task.detach_and_log_err(cx))
|
||||
// .log_err();
|
||||
// }
|
||||
// }
|
||||
pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
room.update(cx, Room::toggle_deafen)
|
||||
.map(|task| task.detach_and_log_err(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
// fn notification_window_options(
|
||||
// screen: Rc<dyn Screen>,
|
||||
// window_size: Vector2F,
|
||||
// ) -> WindowOptions<'static> {
|
||||
// const NOTIFICATION_PADDING: f32 = 16.;
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn PlatformDisplay>,
|
||||
window_size: Size<Pixels>,
|
||||
) -> WindowOptions {
|
||||
let notification_margin_width = GlobalPixels::from(16.);
|
||||
let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.);
|
||||
|
||||
// let screen_bounds = screen.content_bounds();
|
||||
// WindowOptions {
|
||||
// bounds: WindowBounds::Fixed(RectF::new(
|
||||
// screen_bounds.upper_right()
|
||||
// + vec2f(
|
||||
// -NOTIFICATION_PADDING - window_size.x(),
|
||||
// NOTIFICATION_PADDING,
|
||||
// ),
|
||||
// window_size,
|
||||
// )),
|
||||
// titlebar: None,
|
||||
// center: false,
|
||||
// focus: false,
|
||||
// show: true,
|
||||
// kind: WindowKind::PopUp,
|
||||
// is_movable: false,
|
||||
// screen: Some(screen),
|
||||
// }
|
||||
// }
|
||||
let screen_bounds = screen.bounds();
|
||||
let size: Size<GlobalPixels> = window_size.into();
|
||||
|
||||
// todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument.
|
||||
let bounds = gpui::Bounds::<GlobalPixels> {
|
||||
origin: screen_bounds.upper_right()
|
||||
- point(
|
||||
size.width + notification_margin_width,
|
||||
notification_margin_height,
|
||||
),
|
||||
size: window_size.into(),
|
||||
};
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(bounds),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
display_id: Some(screen.id()),
|
||||
}
|
||||
}
|
||||
|
||||
// fn render_avatar<T: 'static>(
|
||||
// avatar: Option<Arc<ImageData>>,
|
||||
@@ -149,6 +158,6 @@ pub fn init(_app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||
// cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||
// }
|
||||
fn is_channels_feature_enabled(cx: &gpui::WindowContext<'_>) -> bool {
|
||||
cx.is_staff() || cx.has_flag::<ChannelsAlpha>()
|
||||
}
|
||||
|
||||
@@ -1,113 +1,32 @@
|
||||
// use std::ops::Range;
|
||||
use gpui::{
|
||||
div, AnyElement, Div, ElementId, IntoElement, ParentElement as _, RenderOnce, Styled,
|
||||
WindowContext,
|
||||
};
|
||||
|
||||
// use gpui::{
|
||||
// geometry::{
|
||||
// rect::RectF,
|
||||
// vector::{vec2f, Vector2F},
|
||||
// },
|
||||
// json::ToJson,
|
||||
// serde_json::{self, json},
|
||||
// AnyElement, Axis, Element, View, ViewContext,
|
||||
// };
|
||||
#[derive(Default, IntoElement)]
|
||||
pub struct FacePile {
|
||||
pub faces: Vec<AnyElement>,
|
||||
}
|
||||
|
||||
// pub(crate) struct FacePile<V: View> {
|
||||
// overlap: f32,
|
||||
// faces: Vec<AnyElement<V>>,
|
||||
// }
|
||||
impl RenderOnce for FacePile {
|
||||
type Rendered = Div;
|
||||
|
||||
// impl<V: View> FacePile<V> {
|
||||
// pub fn new(overlap: f32) -> Self {
|
||||
// Self {
|
||||
// overlap,
|
||||
// faces: Vec::new(),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
fn render(self, _: &mut WindowContext) -> Self::Rendered {
|
||||
let player_count = self.faces.len();
|
||||
let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| {
|
||||
let isnt_last = ix < player_count - 1;
|
||||
|
||||
// impl<V: View> Element<V> for FacePile<V> {
|
||||
// type LayoutState = ();
|
||||
// type PaintState = ();
|
||||
div()
|
||||
.z_index((player_count - ix) as u32)
|
||||
.when(isnt_last, |div| div.neg_mr_1())
|
||||
.child(player)
|
||||
});
|
||||
div().p_1().flex().items_center().children(player_list)
|
||||
}
|
||||
}
|
||||
|
||||
// fn layout(
|
||||
// &mut self,
|
||||
// constraint: gpui::SizeConstraint,
|
||||
// view: &mut V,
|
||||
// cx: &mut ViewContext<V>,
|
||||
// ) -> (Vector2F, Self::LayoutState) {
|
||||
// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
// let mut width = 0.;
|
||||
// let mut max_height = 0.;
|
||||
// for face in &mut self.faces {
|
||||
// let layout = face.layout(constraint, view, cx);
|
||||
// width += layout.x();
|
||||
// max_height = f32::max(max_height, layout.y());
|
||||
// }
|
||||
// width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
// (
|
||||
// Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||
// (),
|
||||
// )
|
||||
// }
|
||||
|
||||
// fn paint(
|
||||
// &mut self,
|
||||
// bounds: RectF,
|
||||
// visible_bounds: RectF,
|
||||
// _layout: &mut Self::LayoutState,
|
||||
// view: &mut V,
|
||||
// cx: &mut ViewContext<V>,
|
||||
// ) -> Self::PaintState {
|
||||
// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
// let origin_y = bounds.upper_right().y();
|
||||
// let mut origin_x = bounds.upper_right().x();
|
||||
|
||||
// for face in self.faces.iter_mut().rev() {
|
||||
// let size = face.size();
|
||||
// origin_x -= size.x();
|
||||
// let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
|
||||
|
||||
// cx.scene().push_layer(None);
|
||||
// face.paint(vec2f(origin_x, origin_y), visible_bounds, view, cx);
|
||||
// cx.scene().pop_layer();
|
||||
// origin_x += self.overlap;
|
||||
// }
|
||||
|
||||
// ()
|
||||
// }
|
||||
|
||||
// fn rect_for_text_range(
|
||||
// &self,
|
||||
// _: Range<usize>,
|
||||
// _: RectF,
|
||||
// _: RectF,
|
||||
// _: &Self::LayoutState,
|
||||
// _: &Self::PaintState,
|
||||
// _: &V,
|
||||
// _: &ViewContext<V>,
|
||||
// ) -> Option<RectF> {
|
||||
// None
|
||||
// }
|
||||
|
||||
// fn debug(
|
||||
// &self,
|
||||
// bounds: RectF,
|
||||
// _: &Self::LayoutState,
|
||||
// _: &Self::PaintState,
|
||||
// _: &V,
|
||||
// _: &ViewContext<V>,
|
||||
// ) -> serde_json::Value {
|
||||
// json!({
|
||||
// "type": "FacePile",
|
||||
// "bounds": bounds.to_json()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
|
||||
// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
||||
// self.faces.extend(children);
|
||||
// }
|
||||
// }
|
||||
impl Extend<AnyElement> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// use gpui::AppContext;
|
||||
// use std::sync::Arc;
|
||||
// use workspace::AppState;
|
||||
use gpui::AppContext;
|
||||
use std::sync::Arc;
|
||||
use workspace::AppState;
|
||||
|
||||
// pub mod incoming_call_notification;
|
||||
// pub mod project_shared_notification;
|
||||
pub mod incoming_call_notification;
|
||||
pub mod project_shared_notification;
|
||||
|
||||
// pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
// incoming_call_notification::init(app_state, cx);
|
||||
// project_shared_notification::init(app_state, cx);
|
||||
// }
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
incoming_call_notification::init(app_state, cx);
|
||||
project_shared_notification::init(app_state, cx);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
use crate::notification_window_options;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use client::proto;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
|
||||
div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext,
|
||||
VisualContext as _, WindowHandle,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, v_stack, Avatar, Button, Label};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
@@ -19,21 +18,33 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = Vec::new();
|
||||
while let Some(incoming_call) = incoming_call.next().await {
|
||||
for window in notification_windows.drain(..) {
|
||||
window.remove(&mut cx);
|
||||
window
|
||||
.update(&mut cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
if let Some(incoming_call) = incoming_call {
|
||||
let window_size = cx.read(|cx| {
|
||||
let theme = &theme::current(cx).incoming_call_notification;
|
||||
vec2f(theme.window_width, theme.window_height)
|
||||
});
|
||||
let unique_screens = cx.update(|cx| cx.displays()).unwrap();
|
||||
let window_size = gpui::Size {
|
||||
width: px(380.),
|
||||
height: px(64.),
|
||||
};
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
for screen in unique_screens {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let window = cx
|
||||
.add_window(notification_window_options(screen, window_size), |_| {
|
||||
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
||||
});
|
||||
|
||||
.open_window(options, |cx| {
|
||||
cx.build_view(|_| {
|
||||
IncomingCallNotification::new(
|
||||
incoming_call.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
notification_windows.push(window);
|
||||
}
|
||||
}
|
||||
@@ -47,167 +58,97 @@ struct RespondToCall {
|
||||
accept: bool,
|
||||
}
|
||||
|
||||
pub struct IncomingCallNotification {
|
||||
struct IncomingCallNotificationState {
|
||||
call: IncomingCall,
|
||||
app_state: Weak<AppState>,
|
||||
}
|
||||
|
||||
impl IncomingCallNotification {
|
||||
pub struct IncomingCallNotification {
|
||||
state: Arc<IncomingCallNotificationState>,
|
||||
}
|
||||
impl IncomingCallNotificationState {
|
||||
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||
Self { call, app_state }
|
||||
}
|
||||
|
||||
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
|
||||
fn respond(&self, accept: bool, cx: &mut AppContext) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
if accept {
|
||||
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||
let caller_user_id = self.call.calling_user.id;
|
||||
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||
let app_state = self.app_state.clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
join.await?;
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let cx: &mut AppContext = cx;
|
||||
cx.spawn(|cx| async move {
|
||||
join.await?;
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
} else {
|
||||
active_call.update(cx, |active_call, cx| {
|
||||
active_call.decline_incoming(cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).incoming_call_notification;
|
||||
let default_project = proto::ParticipantProject::default();
|
||||
let initial_project = self
|
||||
.call
|
||||
.initial_project
|
||||
.as_ref()
|
||||
.unwrap_or(&default_project);
|
||||
Flex::row()
|
||||
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.caller_avatar)
|
||||
.aligned()
|
||||
impl IncomingCallNotification {
|
||||
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||
Self {
|
||||
state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
|
||||
}
|
||||
}
|
||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
h_stack()
|
||||
.children(
|
||||
self.state
|
||||
.call
|
||||
.calling_user
|
||||
.avatar
|
||||
.as_ref()
|
||||
.map(|avatar| Avatar::data(avatar.clone())),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(format!(
|
||||
"{} is sharing a project in Zed",
|
||||
self.state.call.calling_user.github_login
|
||||
)))
|
||||
.child(self.render_buttons(cx)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
h_stack()
|
||||
.child(Button::new("accept", "Accept").render(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(true, cx)
|
||||
}))
|
||||
.child(Button::new("decline", "Decline").render(cx).on_click({
|
||||
let state = self.state.clone();
|
||||
move |_, cx| state.respond(false, cx)
|
||||
}))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(
|
||||
self.call.calling_user.github_login.clone(),
|
||||
theme.caller_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_username.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if initial_project.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
),
|
||||
theme.caller_message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_message.container),
|
||||
)
|
||||
.with_children(if initial_project.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(
|
||||
initial_project.worktree_root_names.join(", "),
|
||||
theme.worktree_roots.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.worktree_roots.container),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.caller_metadata)
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_container)
|
||||
.flex(1., true)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Accept {}
|
||||
enum Decline {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Accept", theme.accept_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.accept_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.respond(true, cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Decline", theme.decline_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.decline_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.respond(false, cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(theme.incoming_call_notification.button_width)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for IncomingCallNotification {
|
||||
type Event = ();
|
||||
}
|
||||
impl Render for IncomingCallNotification {
|
||||
type Element = Div;
|
||||
|
||||
impl View for IncomingCallNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"IncomingCallNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let background = theme::current(cx).incoming_call_notification.background;
|
||||
Flex::row()
|
||||
.with_child(self.render_caller(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.expanded()
|
||||
.into_any()
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
div().bg(red()).flex_none().child(self.render_caller(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, View, ViewContext,
|
||||
px, AppContext, Div, Element, ParentElement, Render, RenderOnce, Size, Styled, ViewContext,
|
||||
VisualContext,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use ui::{h_stack, v_stack, Avatar, Button, Clickable, Label};
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
@@ -21,38 +20,54 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
project_id,
|
||||
worktree_root_names,
|
||||
} => {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||
let window_size = Size {
|
||||
width: px(380.),
|
||||
height: px(64.),
|
||||
};
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let window =
|
||||
cx.add_window(notification_window_options(screen, window_size), |_| {
|
||||
for screen in cx.displays() {
|
||||
let options = notification_window_options(screen, window_size);
|
||||
let window = cx.open_window(options, |cx| {
|
||||
cx.build_view(|_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
});
|
||||
})
|
||||
});
|
||||
notification_windows
|
||||
.entry(*project_id)
|
||||
.or_insert(Vec::new())
|
||||
.push(window);
|
||||
}
|
||||
}
|
||||
|
||||
room::Event::RemoteProjectUnshared { project_id }
|
||||
| room::Event::RemoteProjectJoined { project_id }
|
||||
| room::Event::RemoteProjectInvitationDiscarded { project_id } => {
|
||||
if let Some(windows) = notification_windows.remove(&project_id) {
|
||||
for window in windows {
|
||||
window.remove(cx);
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
room::Event::Left => {
|
||||
for (_, windows) in notification_windows.drain() {
|
||||
for window in windows {
|
||||
window.remove(cx);
|
||||
window
|
||||
.update(cx, |_, cx| {
|
||||
// todo!()
|
||||
cx.remove_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,116 +117,60 @@ impl ProjectSharedNotification {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
Flex::row()
|
||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.owner_avatar)
|
||||
.aligned()
|
||||
}))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(
|
||||
self.owner.github_login.clone(),
|
||||
theme.owner_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_username.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
),
|
||||
theme.message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.message.container),
|
||||
)
|
||||
.with_children(if self.worktree_root_names.is_empty() {
|
||||
fn render_owner(&self) -> impl Element {
|
||||
h_stack()
|
||||
.children(
|
||||
self.owner
|
||||
.avatar
|
||||
.clone()
|
||||
.map(|avatar| Avatar::data(avatar.clone())),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.child(Label::new(self.owner.github_login.clone()))
|
||||
.child(Label::new(format!(
|
||||
"is sharing a project in Zed{}",
|
||||
if self.worktree_root_names.is_empty() {
|
||||
""
|
||||
} else {
|
||||
":"
|
||||
}
|
||||
)))
|
||||
.children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Label::new(
|
||||
self.worktree_root_names.join(", "),
|
||||
theme.worktree_roots.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.worktree_roots.container),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.owner_metadata)
|
||||
.aligned(),
|
||||
Some(Label::new(self.worktree_root_names.join(", ")))
|
||||
}),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_container)
|
||||
.flex(1., true)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Open {}
|
||||
enum Dismiss {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Open", theme.open_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.open_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
|
||||
.flex(1., true),
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> impl Element {
|
||||
let this = cx.view().clone();
|
||||
v_stack()
|
||||
.child(Button::new("open", "Open").render(cx).on_click({
|
||||
let this = this.clone();
|
||||
move |_, cx| {
|
||||
this.update(cx, |this, cx| this.join(cx));
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
Button::new("dismiss", "Dismiss")
|
||||
.render(cx)
|
||||
.on_click(move |_, cx| {
|
||||
this.update(cx, |this, cx| this.dismiss(cx));
|
||||
}),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.dismiss_button.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.dismiss(cx);
|
||||
})
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(theme.project_shared_notification.button_width)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectSharedNotification {
|
||||
type Event = ();
|
||||
}
|
||||
impl Render for ProjectSharedNotification {
|
||||
type Element = Div;
|
||||
|
||||
impl View for ProjectSharedNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"ProjectSharedNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||
let background = theme::current(cx).project_shared_notification.background;
|
||||
Flex::row()
|
||||
.with_child(self.render_owner(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.expanded()
|
||||
.into_any()
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
h_stack()
|
||||
.size_full()
|
||||
.bg(gpui::red())
|
||||
.child(self.render_owner())
|
||||
.child(self.render_buttons(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,13 @@ pub type HashMap<K, V> = std::collections::HashMap<K, V>;
|
||||
#[cfg(not(feature = "test-support"))]
|
||||
pub type HashSet<T> = std::collections::HashSet<T>;
|
||||
|
||||
use std::any::TypeId;
|
||||
pub use std::collections::*;
|
||||
|
||||
// NEW TYPES
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CommandPaletteFilter {
|
||||
pub filtered_namespaces: HashSet<&'static str>,
|
||||
pub hidden_namespaces: HashSet<&'static str>,
|
||||
pub hidden_action_types: HashSet<TypeId>,
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let filtered = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
filter.filtered_namespaces.contains(action.namespace())
|
||||
filter.hidden_namespaces.contains(action.namespace())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
@@ -430,7 +430,7 @@ mod tests {
|
||||
// Add namespace filter, and redeploy the palette
|
||||
cx.update(|cx| {
|
||||
cx.update_default_global::<CommandPaletteFilter, _, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("editor");
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
|
||||
|
||||
use collections::{CommandPaletteFilter, HashMap};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
|
||||
ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
|
||||
use ui::{h_stack, prelude::*, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
ResultExt,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::OpenZedURL;
|
||||
|
||||
actions!(Toggle);
|
||||
@@ -25,6 +26,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(CommandPalette::register).detach();
|
||||
}
|
||||
|
||||
impl ModalView for CommandPalette {}
|
||||
|
||||
pub struct CommandPalette {
|
||||
picker: View<Picker<CommandPaletteDelegate>>,
|
||||
}
|
||||
@@ -48,14 +51,16 @@ impl CommandPalette {
|
||||
.filter_map(|action| {
|
||||
let name = gpui::remove_the_2(action.name());
|
||||
let namespace = name.split("::").next().unwrap_or("malformed action name");
|
||||
if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
|
||||
if filter.is_some_and(|f| {
|
||||
f.hidden_namespaces.contains(namespace)
|
||||
|| f.hidden_action_types.contains(&action.type_id())
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Command {
|
||||
name: humanize_action_name(&name),
|
||||
action,
|
||||
keystrokes: vec![], // todo!()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -68,7 +73,7 @@ impl CommandPalette {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Manager> for CommandPalette {}
|
||||
impl EventEmitter<DismissEvent> for CommandPalette {}
|
||||
|
||||
impl FocusableView for CommandPalette {
|
||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||
@@ -77,10 +82,10 @@ impl FocusableView for CommandPalette {
|
||||
}
|
||||
|
||||
impl Render for CommandPalette {
|
||||
type Output = Div;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Output {
|
||||
v_stack().w_96().child(self.picker.clone())
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
v_stack().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +109,6 @@ pub struct CommandPaletteDelegate {
|
||||
struct Command {
|
||||
name: String,
|
||||
action: Box<dyn Action>,
|
||||
keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
impl Clone for Command {
|
||||
@@ -112,7 +116,6 @@ impl Clone for Command {
|
||||
Self {
|
||||
name: self.name.clone(),
|
||||
action: self.action.boxed_clone(),
|
||||
keystrokes: self.keystrokes.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +143,7 @@ impl CommandPaletteDelegate {
|
||||
}
|
||||
|
||||
impl PickerDelegate for CommandPaletteDelegate {
|
||||
type ListItem = Div;
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Execute a command...".into()
|
||||
@@ -223,6 +226,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CommandInterceptResult {
|
||||
action,
|
||||
string,
|
||||
@@ -238,7 +242,6 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
commands.push(Command {
|
||||
name: string.clone(),
|
||||
action,
|
||||
keystrokes: vec![],
|
||||
});
|
||||
matches.insert(
|
||||
0,
|
||||
@@ -250,6 +253,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
@@ -268,7 +272,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.command_palette
|
||||
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||
.update(cx, |_, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
@@ -279,6 +283,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
}
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
let command = self.commands.swap_remove(action_ix);
|
||||
self.matches.clear();
|
||||
self.commands.clear();
|
||||
cx.update_global(|hit_counts: &mut HitCounts, _| {
|
||||
*hit_counts.0.entry(command.name).or_default() += 1;
|
||||
});
|
||||
@@ -293,32 +299,25 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Self::ListItem {
|
||||
let colors = cx.theme().colors();
|
||||
let Some(r#match) = self.matches.get(ix) else {
|
||||
return div();
|
||||
};
|
||||
let Some(command) = self.commands.get(r#match.candidate_id) else {
|
||||
return div();
|
||||
};
|
||||
|
||||
div()
|
||||
.px_1()
|
||||
.text_color(colors.text)
|
||||
.text_ui()
|
||||
.bg(colors.ghost_element_background)
|
||||
.rounded_md()
|
||||
.when(selected, |this| this.bg(colors.ghost_element_selected))
|
||||
.hover(|this| this.bg(colors.ghost_element_hover))
|
||||
.child(
|
||||
) -> Option<Self::ListItem> {
|
||||
let r#match = self.matches.get(ix)?;
|
||||
let command = self.commands.get(r#match.candidate_id)?;
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).selected(selected).child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(HighlightedLabel::new(
|
||||
command.name.clone(),
|
||||
r#match.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action(&*command.action, cx)),
|
||||
)
|
||||
.children(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
&self.previous_focus_handle,
|
||||
cx,
|
||||
)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,8 +349,7 @@ impl std::fmt::Debug for Command {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Command")
|
||||
.field("name", &self.name)
|
||||
.field("keystrokes", &self.keystrokes)
|
||||
.finish()
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +432,7 @@ mod tests {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(CommandPaletteFilter::default());
|
||||
cx.update_global::<CommandPaletteFilter, _>(|filter, _| {
|
||||
filter.filtered_namespaces.insert("editor");
|
||||
filter.hidden_namespaces.insert("editor");
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ theme = { path = "../theme" }
|
||||
lsp = { path = "../lsp" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -58,16 +58,16 @@ pub fn init(
|
||||
cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
|
||||
match status {
|
||||
Status::Disabled => {
|
||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
Status::Authorized => {
|
||||
filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
_ => {
|
||||
filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
filter.hidden_namespaces.insert(COPILOT_NAMESPACE);
|
||||
filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -28,7 +28,8 @@ theme = { package = "theme2", path = "../theme2" }
|
||||
lsp = { package = "lsp2", path = "../lsp2" }
|
||||
node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
ui = { package = "ui2", path = "../ui2" }
|
||||
async-compression.workspace = true
|
||||
async-tar = "0.4.2"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
@@ -45,6 +46,6 @@ fs = { path = "../fs", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
rpc = { package = "rpc2", path = "../rpc2", features = ["test-support"] }
|
||||
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
||||
@@ -22,6 +22,7 @@ use request::StatusNotification;
|
||||
use settings::SettingsStore;
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
ffi::OsString,
|
||||
mem,
|
||||
ops::Range,
|
||||
@@ -32,13 +33,14 @@ use util::{
|
||||
fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt,
|
||||
};
|
||||
|
||||
// todo!()
|
||||
// const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
|
||||
actions!(SignIn, SignOut);
|
||||
|
||||
// todo!()
|
||||
// const COPILOT_NAMESPACE: &'static str = "copilot";
|
||||
actions!(Suggest, NextSuggestion, PreviousSuggestion, Reinstall);
|
||||
actions!(
|
||||
Suggest,
|
||||
NextSuggestion,
|
||||
PreviousSuggestion,
|
||||
Reinstall,
|
||||
SignIn,
|
||||
SignOut
|
||||
);
|
||||
|
||||
pub fn init(
|
||||
new_server_id: LanguageServerId,
|
||||
@@ -51,52 +53,70 @@ pub fn init(
|
||||
move |cx| Copilot::start(new_server_id, http, node_runtime, cx)
|
||||
});
|
||||
cx.set_global(copilot.clone());
|
||||
cx.observe(&copilot, |handle, cx| {
|
||||
let copilot_action_types = [
|
||||
TypeId::of::<Suggest>(),
|
||||
TypeId::of::<NextSuggestion>(),
|
||||
TypeId::of::<PreviousSuggestion>(),
|
||||
TypeId::of::<Reinstall>(),
|
||||
];
|
||||
let copilot_auth_action_types = [TypeId::of::<SignOut>()];
|
||||
let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
|
||||
let status = handle.read(cx).status();
|
||||
let filter = cx.default_global::<collections::CommandPaletteFilter>();
|
||||
|
||||
// TODO
|
||||
// cx.observe(&copilot, |handle, cx| {
|
||||
// let status = handle.read(cx).status();
|
||||
// cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
|
||||
// match status {
|
||||
// Status::Disabled => {
|
||||
// filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// Status::Authorized => {
|
||||
// filter.filtered_namespaces.remove(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// _ => {
|
||||
// filter.filtered_namespaces.insert(COPILOT_NAMESPACE);
|
||||
// filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// })
|
||||
// .detach();
|
||||
match status {
|
||||
Status::Disabled => {
|
||||
filter.hidden_action_types.extend(copilot_action_types);
|
||||
filter.hidden_action_types.extend(copilot_auth_action_types);
|
||||
filter
|
||||
.hidden_action_types
|
||||
.extend(copilot_no_auth_action_types);
|
||||
}
|
||||
Status::Authorized => {
|
||||
filter
|
||||
.hidden_action_types
|
||||
.extend(copilot_no_auth_action_types);
|
||||
for type_id in copilot_action_types
|
||||
.iter()
|
||||
.chain(&copilot_auth_action_types)
|
||||
{
|
||||
filter.hidden_action_types.remove(type_id);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
filter.hidden_action_types.extend(copilot_action_types);
|
||||
filter.hidden_action_types.extend(copilot_auth_action_types);
|
||||
for type_id in &copilot_no_auth_action_types {
|
||||
filter.hidden_action_types.remove(type_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// sign_in::init(cx);
|
||||
// cx.add_global_action(|_: &SignIn, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// });
|
||||
// cx.add_global_action(|_: &SignOut, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
// .detach_and_log_err(cx);
|
||||
// }
|
||||
// });
|
||||
|
||||
// cx.add_global_action(|_: &Reinstall, cx| {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
// .detach();
|
||||
// }
|
||||
// });
|
||||
sign_in::init(cx);
|
||||
cx.on_action(|_: &SignIn, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.on_action(|_: &SignOut, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
cx.on_action(|_: &Reinstall, cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
enum CopilotServer {
|
||||
@@ -1002,229 +1022,231 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// use super::*;
|
||||
// use gpui::{executor::Deterministic, TestAppContext};
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
// #[gpui::test(iterations = 10)]
|
||||
// async fn test_buffer_management(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
// deterministic.forbid_parking();
|
||||
// let (copilot, mut lsp) = Copilot::fake(cx);
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_management(cx: &mut TestAppContext) {
|
||||
let (copilot, mut lsp) = Copilot::fake(cx);
|
||||
|
||||
// let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello"));
|
||||
// let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap();
|
||||
// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Hello".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
let buffer_1 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Hello"));
|
||||
let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.entity_id().as_u64())
|
||||
.parse()
|
||||
.unwrap();
|
||||
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Hello".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye"));
|
||||
// let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap();
|
||||
// copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_2_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Goodbye".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
let buffer_2 = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), "Goodbye"));
|
||||
let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.entity_id().as_u64())
|
||||
.parse()
|
||||
.unwrap();
|
||||
copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_2_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Goodbye".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidChangeTextDocumentParams {
|
||||
// text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
|
||||
// content_changes: vec![lsp::TextDocumentContentChangeEvent {
|
||||
// range: Some(lsp::Range::new(
|
||||
// lsp::Position::new(0, 5),
|
||||
// lsp::Position::new(0, 5)
|
||||
// )),
|
||||
// range_length: None,
|
||||
// text: " world".into(),
|
||||
// }],
|
||||
// }
|
||||
// );
|
||||
buffer_1.update(cx, |buffer, cx| buffer.edit([(5..5, " world")], None, cx));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidChangeTextDocument>()
|
||||
.await,
|
||||
lsp::DidChangeTextDocumentParams {
|
||||
text_document: lsp::VersionedTextDocumentIdentifier::new(buffer_1_uri.clone(), 1),
|
||||
content_changes: vec![lsp::TextDocumentContentChangeEvent {
|
||||
range: Some(lsp::Range::new(
|
||||
lsp::Position::new(0, 5),
|
||||
lsp::Position::new(0, 5)
|
||||
)),
|
||||
range_length: None,
|
||||
text: " world".into(),
|
||||
}],
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure updates to the file are reflected in the LSP.
|
||||
// buffer_1
|
||||
// .update(cx, |buffer, cx| {
|
||||
// buffer.file_updated(
|
||||
// Arc::new(File {
|
||||
// abs_path: "/root/child/buffer-1".into(),
|
||||
// path: Path::new("child/buffer-1").into(),
|
||||
// }),
|
||||
// cx,
|
||||
// )
|
||||
// })
|
||||
// .await;
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||
// }
|
||||
// );
|
||||
// let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 1,
|
||||
// "Hello world".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// Ensure updates to the file are reflected in the LSP.
|
||||
buffer_1.update(cx, |buffer, cx| {
|
||||
buffer.file_updated(
|
||||
Arc::new(File {
|
||||
abs_path: "/root/child/buffer-1".into(),
|
||||
path: Path::new("child/buffer-1").into(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri),
|
||||
}
|
||||
);
|
||||
let buffer_1_uri = lsp::Url::from_file_path("/root/child/buffer-1").unwrap();
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
1,
|
||||
"Hello world".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure all previously-registered buffers are closed when signing out.
|
||||
// lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
|
||||
// Ok(request::SignOutResult {})
|
||||
// });
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
// }
|
||||
// );
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
// }
|
||||
// );
|
||||
// Ensure all previously-registered buffers are closed when signing out.
|
||||
lsp.handle_request::<request::SignOut, _, _>(|_, _| async {
|
||||
Ok(request::SignOutResult {})
|
||||
});
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_out(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
// todo!() po: these notifications now happen in reverse order?
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_1_uri.clone()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri.clone()),
|
||||
}
|
||||
);
|
||||
|
||||
// // Ensure all previously-registered buffers are re-opened when signing in.
|
||||
// lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
|
||||
// Ok(request::SignInInitiateResult::AlreadySignedIn {
|
||||
// user: "user-1".into(),
|
||||
// })
|
||||
// });
|
||||
// copilot
|
||||
// .update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
// .await
|
||||
// .unwrap();
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_2_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Goodbye".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidOpenTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentItem::new(
|
||||
// buffer_1_uri.clone(),
|
||||
// "plaintext".into(),
|
||||
// 0,
|
||||
// "Hello world".into()
|
||||
// ),
|
||||
// }
|
||||
// );
|
||||
// Ensure all previously-registered buffers are re-opened when signing in.
|
||||
lsp.handle_request::<request::SignInInitiate, _, _>(|_, _| async {
|
||||
Ok(request::SignInInitiateResult::AlreadySignedIn {
|
||||
user: "user-1".into(),
|
||||
})
|
||||
});
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// // Dropping a buffer causes it to be closed on the LSP side as well.
|
||||
// cx.update(|_| drop(buffer_2));
|
||||
// assert_eq!(
|
||||
// lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
// .await,
|
||||
// lsp::DidCloseTextDocumentParams {
|
||||
// text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_1_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Hello world".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await,
|
||||
lsp::DidOpenTextDocumentParams {
|
||||
text_document: lsp::TextDocumentItem::new(
|
||||
buffer_2_uri.clone(),
|
||||
"plaintext".into(),
|
||||
0,
|
||||
"Goodbye".into()
|
||||
),
|
||||
}
|
||||
);
|
||||
// Dropping a buffer causes it to be closed on the LSP side as well.
|
||||
cx.update(|_| drop(buffer_2));
|
||||
assert_eq!(
|
||||
lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
|
||||
.await,
|
||||
lsp::DidCloseTextDocumentParams {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer_2_uri),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// struct File {
|
||||
// abs_path: PathBuf,
|
||||
// path: Arc<Path>,
|
||||
// }
|
||||
struct File {
|
||||
abs_path: PathBuf,
|
||||
path: Arc<Path>,
|
||||
}
|
||||
|
||||
// impl language2::File for File {
|
||||
// fn as_local(&self) -> Option<&dyn language2::LocalFile> {
|
||||
// Some(self)
|
||||
// }
|
||||
impl language::File for File {
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
// fn mtime(&self) -> std::time::SystemTime {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn mtime(&self) -> std::time::SystemTime {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn path(&self) -> &Arc<Path> {
|
||||
// &self.path
|
||||
// }
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
// fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn full_path(&self, _: &AppContext) -> PathBuf {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn file_name<'a>(&'a self, _: &'a AppContext) -> &'a std::ffi::OsStr {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn is_deleted(&self) -> bool {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn is_deleted(&self) -> bool {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn as_any(&self) -> &dyn std::any::Any {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn to_proto(&self) -> rpc::proto::File {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn worktree_id(&self) -> usize {
|
||||
// 0
|
||||
// }
|
||||
// }
|
||||
fn worktree_id(&self) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
// impl language::LocalFile for File {
|
||||
// fn abs_path(&self, _: &AppContext) -> PathBuf {
|
||||
// self.abs_path.clone()
|
||||
// }
|
||||
impl language::LocalFile for File {
|
||||
fn abs_path(&self, _: &AppContext) -> PathBuf {
|
||||
self.abs_path.clone()
|
||||
}
|
||||
|
||||
// fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
// unimplemented!()
|
||||
// }
|
||||
fn load(&self, _: &AppContext) -> Task<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
// fn buffer_reloaded(
|
||||
// &self,
|
||||
// _: u64,
|
||||
// _: &clock::Global,
|
||||
// _: language::RopeFingerprint,
|
||||
// _: language::LineEnding,
|
||||
// _: std::time::SystemTime,
|
||||
// _: &mut AppContext,
|
||||
// ) {
|
||||
// unimplemented!()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
fn buffer_reloaded(
|
||||
&self,
|
||||
_: u64,
|
||||
_: &clock::Global,
|
||||
_: language::RopeFingerprint,
|
||||
_: language::LineEnding,
|
||||
_: std::time::SystemTime,
|
||||
_: &mut AppContext,
|
||||
) {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,376 +1,213 @@
|
||||
// TODO add logging in
|
||||
// use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
// use gpui::{
|
||||
// elements::*,
|
||||
// geometry::rect::RectF,
|
||||
// platform::{WindowBounds, WindowKind, WindowOptions},
|
||||
// AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
|
||||
// WindowHandle,
|
||||
// };
|
||||
// use theme::ui::modal;
|
||||
use crate::{request::PromptUserDeviceFlow, Copilot, Status};
|
||||
use gpui::{
|
||||
div, size, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, InteractiveElement,
|
||||
IntoElement, ParentElement, Point, Render, Stateful, Styled, ViewContext, VisualContext,
|
||||
WindowBounds, WindowHandle, WindowKind, WindowOptions,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, Button, Icon, IconElement, Label};
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct CopyUserCode;
|
||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
|
||||
// #[derive(PartialEq, Eq, Debug, Clone)]
|
||||
// struct OpenGithub;
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
|
||||
cx.observe(&copilot, move |copilot, cx| {
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
// const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
match &status {
|
||||
crate::Status::SigningIn { prompt } => {
|
||||
if let Some(window) = verification_window.as_mut() {
|
||||
let updated = window
|
||||
.update(cx, |verification, cx| {
|
||||
verification.set_status(status.clone(), cx);
|
||||
cx.activate_window();
|
||||
})
|
||||
.is_ok();
|
||||
if !updated {
|
||||
verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
} else if let Some(_prompt) = prompt {
|
||||
verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
}
|
||||
Status::Authorized | Status::Unauthorized => {
|
||||
if let Some(window) = verification_window.as_ref() {
|
||||
window
|
||||
.update(cx, |verification, cx| {
|
||||
verification.set_status(status, cx);
|
||||
cx.activate(true);
|
||||
cx.activate_window();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(code_verification) = verification_window.take() {
|
||||
code_verification
|
||||
.update(cx, |_, cx| cx.remove_window())
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn init(cx: &mut AppContext) {
|
||||
// if let Some(copilot) = Copilot::global(cx) {
|
||||
// let mut verification_window: Option<WindowHandle<CopilotCodeVerification>> = None;
|
||||
// cx.observe(&copilot, move |copilot, cx| {
|
||||
// let status = copilot.read(cx).status();
|
||||
fn create_copilot_auth_window(
|
||||
cx: &mut AppContext,
|
||||
status: &Status,
|
||||
) -> WindowHandle<CopilotCodeVerification> {
|
||||
let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.));
|
||||
let window_options = WindowOptions {
|
||||
bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)),
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: true,
|
||||
display_id: None,
|
||||
};
|
||||
let window = cx.open_window(window_options, |cx| {
|
||||
cx.build_view(|_| CopilotCodeVerification::new(status.clone()))
|
||||
});
|
||||
window
|
||||
}
|
||||
|
||||
// match &status {
|
||||
// crate::Status::SigningIn { prompt } => {
|
||||
// if let Some(window) = verification_window.as_mut() {
|
||||
// let updated = window
|
||||
// .root(cx)
|
||||
// .map(|root| {
|
||||
// root.update(cx, |verification, cx| {
|
||||
// verification.set_status(status.clone(), cx);
|
||||
// cx.activate_window();
|
||||
// })
|
||||
// })
|
||||
// .is_some();
|
||||
// if !updated {
|
||||
// verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
// }
|
||||
// } else if let Some(_prompt) = prompt {
|
||||
// verification_window = Some(create_copilot_auth_window(cx, &status));
|
||||
// }
|
||||
// }
|
||||
// Status::Authorized | Status::Unauthorized => {
|
||||
// if let Some(window) = verification_window.as_ref() {
|
||||
// if let Some(verification) = window.root(cx) {
|
||||
// verification.update(cx, |verification, cx| {
|
||||
// verification.set_status(status, cx);
|
||||
// cx.platform().activate(true);
|
||||
// cx.activate_window();
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// _ => {
|
||||
// if let Some(code_verification) = verification_window.take() {
|
||||
// code_verification.update(cx, |cx| cx.remove_window());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// }
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
}
|
||||
|
||||
// fn create_copilot_auth_window(
|
||||
// cx: &mut AppContext,
|
||||
// status: &Status,
|
||||
// ) -> WindowHandle<CopilotCodeVerification> {
|
||||
// let window_size = theme::current(cx).copilot.modal.dimensions();
|
||||
// let window_options = WindowOptions {
|
||||
// bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
|
||||
// titlebar: None,
|
||||
// center: true,
|
||||
// focus: true,
|
||||
// show: true,
|
||||
// kind: WindowKind::Normal,
|
||||
// is_movable: true,
|
||||
// screen: None,
|
||||
// };
|
||||
// cx.add_window(window_options, |_cx| {
|
||||
// CopilotCodeVerification::new(status.clone())
|
||||
// })
|
||||
// }
|
||||
impl CopilotCodeVerification {
|
||||
pub fn new(status: Status) -> Self {
|
||||
Self {
|
||||
status,
|
||||
connect_clicked: false,
|
||||
}
|
||||
}
|
||||
|
||||
// pub struct CopilotCodeVerification {
|
||||
// status: Status,
|
||||
// connect_clicked: bool,
|
||||
// }
|
||||
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
self.status = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// impl CopilotCodeVerification {
|
||||
// pub fn new(status: Status) -> Self {
|
||||
// Self {
|
||||
// status,
|
||||
// connect_clicked: false,
|
||||
// }
|
||||
// }
|
||||
fn render_device_code(
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl IntoElement {
|
||||
let copied = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text() == &data.user_code)
|
||||
.unwrap_or(false);
|
||||
h_stack()
|
||||
.cursor_pointer()
|
||||
.justify_between()
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
let user_code = data.user_code.clone();
|
||||
move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.child(Label::new(data.user_code.clone()))
|
||||
.child(div())
|
||||
.child(Label::new(if copied { "Copied!" } else { "Copy" }))
|
||||
}
|
||||
|
||||
// pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
// self.status = status;
|
||||
// cx.notify();
|
||||
// }
|
||||
fn render_prompting_modal(
|
||||
connect_clicked: bool,
|
||||
data: &PromptUserDeviceFlow,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element {
|
||||
let connect_button_label = if connect_clicked {
|
||||
"Waiting for connection..."
|
||||
} else {
|
||||
"Connect to Github"
|
||||
};
|
||||
v_stack()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.child(Label::new(
|
||||
"Enable Copilot by connecting your existing license",
|
||||
))
|
||||
.child(Self::render_device_code(data, cx))
|
||||
.child(
|
||||
Label::new("Paste this code into GitHub after clicking the button below.")
|
||||
.size(ui::LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Button::new("connect-button", connect_button_label).on_click({
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
cx.listener(move |this, _, cx| {
|
||||
cx.open_url(&verification_uri);
|
||||
this.connect_clicked = true;
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
fn render_enabled_modal() -> impl Element {
|
||||
v_stack()
|
||||
.child(Label::new("Copilot Enabled!"))
|
||||
.child(Label::new(
|
||||
"You can update your settings or sign out from the Copilot menu in the status bar.",
|
||||
))
|
||||
.child(
|
||||
Button::new("copilot-enabled-done-button", "Done")
|
||||
.on_click(|_, cx| cx.remove_window()),
|
||||
)
|
||||
}
|
||||
|
||||
// fn render_device_code(
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> impl IntoAnyElement<Self> {
|
||||
// let copied = cx
|
||||
// .read_from_clipboard()
|
||||
// .map(|item| item.text() == &data.user_code)
|
||||
// .unwrap_or(false);
|
||||
fn render_unauthorized_modal() -> impl Element {
|
||||
v_stack()
|
||||
.child(Label::new(
|
||||
"Enable Copilot by connecting your existing license.",
|
||||
))
|
||||
.child(
|
||||
Label::new("You must have an active Copilot license to use it in Zed.")
|
||||
.color(Color::Warning),
|
||||
)
|
||||
.child(
|
||||
Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| {
|
||||
cx.remove_window();
|
||||
cx.open_url(COPILOT_SIGN_UP_URL)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// let device_code_style = &style.auth.prompting.device_code;
|
||||
impl Render for CopilotCodeVerification {
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
// MouseEventHandler::new::<Self, _>(0, cx, |state, _cx| {
|
||||
// Flex::row()
|
||||
// .with_child(
|
||||
// Label::new(data.user_code.clone(), device_code_style.text.clone())
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(device_code_style.left_container)
|
||||
// .constrained()
|
||||
// .with_width(device_code_style.left),
|
||||
// )
|
||||
// .with_child(
|
||||
// Label::new(
|
||||
// if copied { "Copied!" } else { "Copy" },
|
||||
// device_code_style.cta.style_for(state).text.clone(),
|
||||
// )
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(*device_code_style.right_container.style_for(state))
|
||||
// .constrained()
|
||||
// .with_width(device_code_style.right),
|
||||
// )
|
||||
// .contained()
|
||||
// .with_style(device_code_style.cta.style_for(state).container)
|
||||
// })
|
||||
// .on_click(gpui::platform::MouseButton::Left, {
|
||||
// let user_code = data.user_code.clone();
|
||||
// move |_, _, cx| {
|
||||
// cx.platform()
|
||||
// .write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
// cx.notify();
|
||||
// }
|
||||
// })
|
||||
// .with_cursor_style(gpui::platform::CursorStyle::PointingHand)
|
||||
// }
|
||||
|
||||
// fn render_prompting_modal(
|
||||
// connect_clicked: bool,
|
||||
// data: &PromptUserDeviceFlow,
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum ConnectButton {}
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Enable Copilot by connecting",
|
||||
// style.auth.prompting.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "your existing license.",
|
||||
// style.auth.prompting.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.auth.prompting.subheading.container),
|
||||
// )
|
||||
// .with_child(Self::render_device_code(data, &style, cx))
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Paste this code into GitHub after",
|
||||
// style.auth.prompting.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "clicking the button below.",
|
||||
// style.auth.prompting.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(style.auth.prompting.hint.container.clone()),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
|
||||
// if connect_clicked {
|
||||
// "Waiting for connection..."
|
||||
// } else {
|
||||
// "Connect to GitHub"
|
||||
// },
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// {
|
||||
// let verification_uri = data.verification_uri.clone();
|
||||
// move |_, verification, cx| {
|
||||
// cx.platform().open_url(&verification_uri);
|
||||
// verification.connect_clicked = true;
|
||||
// }
|
||||
// },
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_enabled_modal(
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// enum DoneButton {}
|
||||
|
||||
// let enabled_style = &style.auth.authorized;
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
|
||||
// .contained()
|
||||
// .with_style(enabled_style.subheading.container)
|
||||
// .aligned(),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "You can update your settings or",
|
||||
// enabled_style.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "sign out from the Copilot menu in",
|
||||
// enabled_style.hint.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(enabled_style.hint.container),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
|
||||
// "Done",
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// |_, _, cx| cx.remove_window(),
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
|
||||
// fn render_unauthorized_modal(
|
||||
// style: &theme::Copilot,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> AnyElement<Self> {
|
||||
// let unauthorized_style = &style.auth.not_authorized;
|
||||
|
||||
// Flex::column()
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "Enable Copilot by connecting",
|
||||
// unauthorized_style.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "your existing license.",
|
||||
// unauthorized_style.subheading.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(unauthorized_style.subheading.container),
|
||||
// )
|
||||
// .with_child(
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// Label::new(
|
||||
// "You must have an active copilot",
|
||||
// unauthorized_style.warning.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// Label::new(
|
||||
// "license to use it in Zed.",
|
||||
// unauthorized_style.warning.text.clone(),
|
||||
// )
|
||||
// .aligned(),
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// .contained()
|
||||
// .with_style(unauthorized_style.warning.container),
|
||||
// )
|
||||
// .with_child(theme::ui::cta_button::<Self, _, _, _>(
|
||||
// "Subscribe on GitHub",
|
||||
// style.auth.content_width,
|
||||
// &style.auth.cta_button,
|
||||
// cx,
|
||||
// |_, _, cx| {
|
||||
// cx.remove_window();
|
||||
// cx.platform().open_url(COPILOT_SIGN_UP_URL)
|
||||
// },
|
||||
// ))
|
||||
// .align_children_center()
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
|
||||
// impl Entity for CopilotCodeVerification {
|
||||
// type Event = ();
|
||||
// }
|
||||
|
||||
// impl View for CopilotCodeVerification {
|
||||
// fn ui_name() -> &'static str {
|
||||
// "CopilotCodeVerification"
|
||||
// }
|
||||
|
||||
// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// cx.notify()
|
||||
// }
|
||||
|
||||
// fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
// cx.notify()
|
||||
// }
|
||||
|
||||
// fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
// enum ConnectModal {}
|
||||
|
||||
// let style = theme::current(cx).clone();
|
||||
|
||||
// modal::<ConnectModal, _, _, _, _>(
|
||||
// "Connect Copilot to Zed",
|
||||
// &style.copilot.modal,
|
||||
// cx,
|
||||
// |cx| {
|
||||
// Flex::column()
|
||||
// .with_children([
|
||||
// theme::ui::icon(&style.copilot.auth.header).into_any(),
|
||||
// match &self.status {
|
||||
// Status::SigningIn {
|
||||
// prompt: Some(prompt),
|
||||
// } => Self::render_prompting_modal(
|
||||
// self.connect_clicked,
|
||||
// &prompt,
|
||||
// &style.copilot,
|
||||
// cx,
|
||||
// ),
|
||||
// Status::Unauthorized => {
|
||||
// self.connect_clicked = false;
|
||||
// Self::render_unauthorized_modal(&style.copilot, cx)
|
||||
// }
|
||||
// Status::Authorized => {
|
||||
// self.connect_clicked = false;
|
||||
// Self::render_enabled_modal(&style.copilot, cx)
|
||||
// }
|
||||
// _ => Empty::new().into_any(),
|
||||
// },
|
||||
// ])
|
||||
// .align_children_center()
|
||||
// },
|
||||
// )
|
||||
// .into_any()
|
||||
// }
|
||||
// }
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let prompt = match &self.status {
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(),
|
||||
Status::Unauthorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_unauthorized_modal().into_any_element()
|
||||
}
|
||||
Status::Authorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_enabled_modal().into_any_element()
|
||||
}
|
||||
_ => div().into_any_element(),
|
||||
};
|
||||
div()
|
||||
.id("copilot code verification")
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.p_10()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.child(ui::Label::new("Connect Copilot to Zed"))
|
||||
.child(IconElement::new(Icon::ZedXCopilot))
|
||||
.child(prompt)
|
||||
}
|
||||
}
|
||||
|
||||
27
crates/copilot_button2/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "copilot_button2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/copilot_button.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
copilot = { package = "copilot2", path = "../copilot2" }
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fs = { package = "fs2", path = "../fs2" }
|
||||
zed-actions = { package="zed_actions2", path = "../zed_actions2"}
|
||||
gpui = { package = "gpui2", path = "../gpui2" }
|
||||
language = { package = "language2", path = "../language2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
anyhow.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
370
crates/copilot_button2/src/copilot_button.rs
Normal file
@@ -0,0 +1,370 @@
|
||||
#![allow(unused)]
|
||||
use anyhow::Result;
|
||||
use copilot::{Copilot, SignOut, Status};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
div, Action, AnchorCorner, AppContext, AsyncAppContext, AsyncWindowContext, Div, Entity,
|
||||
ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings, AllLanguageSettings},
|
||||
File, Language,
|
||||
};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
use workspace::{
|
||||
create_and_open_local_file,
|
||||
item::ItemHandle,
|
||||
ui::{
|
||||
popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, PopoverMenu, Tooltip,
|
||||
},
|
||||
StatusItemView, Toast, Workspace,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
const COPILOT_STARTING_TOAST_ID: usize = 1337;
|
||||
const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||
|
||||
pub struct CopilotButton {
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<Language>>,
|
||||
file: Option<Arc<dyn File>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl Render for CopilotButton {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let all_language_settings = all_language_settings(None, cx);
|
||||
if !all_language_settings.copilot.feature_enabled {
|
||||
return div();
|
||||
}
|
||||
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return div();
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
let enabled = self
|
||||
.editor_enabled
|
||||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||
|
||||
let icon = match status {
|
||||
Status::Error(_) => Icon::CopilotError,
|
||||
Status::Authorized => {
|
||||
if enabled {
|
||||
Icon::Copilot
|
||||
} else {
|
||||
Icon::CopilotDisabled
|
||||
}
|
||||
}
|
||||
_ => Icon::CopilotInit,
|
||||
};
|
||||
|
||||
if let Status::Error(e) = status {
|
||||
return div().child(
|
||||
IconButton::new("copilot-error", icon)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
COPILOT_ERROR_TOAST_ID,
|
||||
format!("Copilot can't be started: {}", e),
|
||||
)
|
||||
.on_click(
|
||||
"Reinstall Copilot",
|
||||
|cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.reinstall(cx))
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
);
|
||||
}
|
||||
let this = cx.view().clone();
|
||||
|
||||
div().child(
|
||||
popover_menu("copilot")
|
||||
.menu(move |cx| match status {
|
||||
Status::Authorized => this.update(cx, |this, cx| this.build_copilot_menu(cx)),
|
||||
_ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
|
||||
})
|
||||
.anchor(AnchorCorner::BottomRight)
|
||||
.trigger(
|
||||
IconButton::new("copilot-icon", icon)
|
||||
.tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
|
||||
|
||||
cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
file: None,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
ContextMenu::build(cx, |menu, cx| {
|
||||
menu.entry("Sign In", initiate_sign_in)
|
||||
.entry("Disable Copilot", move |cx| hide_copilot(fs.clone(), cx))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
return ContextMenu::build(cx, move |mut menu, cx| {
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(&language), None, cx)
|
||||
.show_copilot_suggestions;
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language.name()
|
||||
),
|
||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||
);
|
||||
}
|
||||
|
||||
let settings = AllLanguageSettings::get_global(cx);
|
||||
|
||||
if let Some(file) = &self.file {
|
||||
let path = file.path().clone();
|
||||
let path_enabled = settings.copilot_enabled_for_path(&path);
|
||||
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} Suggestions for This Path",
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
move |cx| {
|
||||
if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
|
||||
if let Ok(workspace) = workspace.root_view(cx) {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.spawn(|cx| {
|
||||
configure_disabled_globs(
|
||||
workspace,
|
||||
path_enabled.then_some(path.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let globally_enabled = settings.copilot_enabled(None, None);
|
||||
menu.entry(
|
||||
if globally_enabled {
|
||||
"Hide Suggestions for All Files"
|
||||
} else {
|
||||
"Show Suggestions for All Files"
|
||||
},
|
||||
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
||||
)
|
||||
.separator()
|
||||
.link(
|
||||
"Copilot Settings",
|
||||
OpenBrowser {
|
||||
url: COPILOT_SETTINGS_URL.to_string(),
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
.action("Sign Out", SignOut.boxed_clone())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
let language = snapshot.language_at(suggestion_anchor);
|
||||
let file = snapshot.file_at(suggestion_anchor).cloned();
|
||||
|
||||
self.editor_enabled = Some(
|
||||
all_language_settings(self.file.as_ref(), cx)
|
||||
.copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
|
||||
);
|
||||
self.language = language.cloned();
|
||||
self.file = file;
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CopilotButton {
|
||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
|
||||
self.editor_subscription = Some((
|
||||
cx.observe(&editor, Self::update_enabled),
|
||||
editor.entity_id().as_u64() as usize,
|
||||
));
|
||||
self.update_enabled(editor, cx);
|
||||
} else {
|
||||
self.language = None;
|
||||
self.editor_subscription = None;
|
||||
self.editor_enabled = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure_disabled_globs(
|
||||
workspace: WeakView<Workspace>,
|
||||
path_to_disable: Option<Arc<Path>>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content().as_ref().into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
|
||||
let copilot = file.copilot.get_or_insert_with(Default::default);
|
||||
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
||||
settings
|
||||
.get::<AllLanguageSettings>(None)
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.iter()
|
||||
.map(|glob| glob.glob().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(path_to_disable) = &path_to_disable {
|
||||
globs.push(path_to_disable.to_string_lossy().into_owned());
|
||||
} else {
|
||||
globs.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if !edits.is_empty() {
|
||||
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
|
||||
});
|
||||
|
||||
// When *enabling* a path, don't actually perform an edit, just select the range.
|
||||
if path_to_disable.is_some() {
|
||||
item.edit(edits.iter().cloned(), cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions =
|
||||
all_language_settings(None, cx).copilot_enabled(Some(&language), None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.languages
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||
});
|
||||
}
|
||||
|
||||
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.features.get_or_insert(Default::default()).copilot = Some(false);
|
||||
});
|
||||
}
|
||||
|
||||
fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(workspace) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
|
||||
cx,
|
||||
);
|
||||
workspace.weak_handle()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ impl View for ProjectDiagnosticsEditor {
|
||||
json!({
|
||||
"project": json!({
|
||||
"language_servers": project.language_server_statuses().collect::<Vec<_>>(),
|
||||
"summary": project.diagnostic_summary(cx),
|
||||
"summary": project.diagnostic_summary(false, cx),
|
||||
}),
|
||||
"summary": self.summary,
|
||||
"paths_to_update": self.paths_to_update.iter().map(|(server_id, paths)|
|
||||
@@ -195,7 +195,7 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
|
||||
let project = project_handle.read(cx);
|
||||
let summary = project.diagnostic_summary(cx);
|
||||
let summary = project.diagnostic_summary(false, cx);
|
||||
let mut this = Self {
|
||||
project: project_handle,
|
||||
summary,
|
||||
@@ -241,7 +241,7 @@ impl ProjectDiagnosticsEditor {
|
||||
let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
|
||||
.project
|
||||
.read(cx)
|
||||
.diagnostic_summaries(cx)
|
||||
.diagnostic_summaries(false, cx)
|
||||
.fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
|
||||
summaries.entry(server_id).or_default().insert(path);
|
||||
summaries
|
||||
@@ -320,7 +320,7 @@ impl ProjectDiagnosticsEditor {
|
||||
.context("rechecking diagnostics for paths")?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.summary = this.project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = this.project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(Event::TitleChanged);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
|
||||
@@ -34,19 +34,19 @@ impl DiagnosticIndicator {
|
||||
}
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(cx),
|
||||
summary: project.read(cx).diagnostic_summary(false, cx),
|
||||
in_progress_checks: project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
|
||||
@@ -36,7 +36,7 @@ use std::{
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
pub use toolbar_controls::ToolbarControls;
|
||||
use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
|
||||
use ui::{h_stack, prelude::*, HighlightedLabel, Icon, IconElement, Label};
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
@@ -88,12 +88,12 @@ struct DiagnosticGroupState {
|
||||
block_count: usize,
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
impl Render for ProjectDiagnosticsEditor {
|
||||
type Output = Focusable<Div>;
|
||||
type Element = Focusable<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Output {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let child = if self.path_states.is_empty() {
|
||||
div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
@@ -158,14 +158,14 @@ impl ProjectDiagnosticsEditor {
|
||||
});
|
||||
let editor_event_subscription =
|
||||
cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
|
||||
Self::emit_item_event_for_editor_event(event, cx);
|
||||
cx.emit(event.clone());
|
||||
if event == &EditorEvent::Focused && this.path_states.is_empty() {
|
||||
cx.focus(&this.focus_handle);
|
||||
}
|
||||
});
|
||||
|
||||
let project = project_handle.read(cx);
|
||||
let summary = project.diagnostic_summary(cx);
|
||||
let summary = project.diagnostic_summary(false, cx);
|
||||
let mut this = Self {
|
||||
project: project_handle,
|
||||
summary,
|
||||
@@ -183,40 +183,6 @@ impl ProjectDiagnosticsEditor {
|
||||
this
|
||||
}
|
||||
|
||||
fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
|
||||
match event {
|
||||
EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
|
||||
|
||||
EditorEvent::Saved | EditorEvent::TitleChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::Reparsed => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::SelectionsChanged { local } if *local => {
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::DirtyChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
EditorEvent::BufferEdited => {
|
||||
cx.emit(ItemEvent::Edit);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
|
||||
cx.emit(ItemEvent::Edit);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
|
||||
workspace.activate_item(&existing, cx);
|
||||
@@ -252,7 +218,7 @@ impl ProjectDiagnosticsEditor {
|
||||
let mut new_summaries: HashMap<LanguageServerId, HashSet<ProjectPath>> = self
|
||||
.project
|
||||
.read(cx)
|
||||
.diagnostic_summaries(cx)
|
||||
.diagnostic_summaries(false, cx)
|
||||
.fold(HashMap::default(), |mut summaries, (path, server_id, _)| {
|
||||
summaries.entry(server_id).or_default().insert(path);
|
||||
summaries
|
||||
@@ -332,9 +298,8 @@ impl ProjectDiagnosticsEditor {
|
||||
.context("rechecking diagnostics for paths")?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.summary = this.project.read(cx).diagnostic_summary(cx);
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
cx.emit(ItemEvent::UpdateBreadcrumbs);
|
||||
this.summary = this.project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
@@ -649,6 +614,12 @@ impl FocusableView for ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
impl Item for ProjectDiagnosticsEditor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.deactivated(cx));
|
||||
}
|
||||
@@ -774,24 +745,39 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
Arc::new(move |_| {
|
||||
h_stack()
|
||||
.id("diagnostic header")
|
||||
.gap_3()
|
||||
.bg(gpui::red())
|
||||
.map(|stack| {
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||
} else {
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||
};
|
||||
|
||||
stack.child(div().pl_8().child(icon))
|
||||
})
|
||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||
stack.child(Label::new(format!("{source}:")).color(Color::Accent))
|
||||
})
|
||||
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(Label::new(code.clone()))
|
||||
})
|
||||
.py_2()
|
||||
.pl_10()
|
||||
.pr_5()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_3()
|
||||
.map(|stack| {
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
IconElement::new(Icon::XCircle).color(Color::Error)
|
||||
} else {
|
||||
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
|
||||
};
|
||||
stack.child(icon)
|
||||
})
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
|
||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
||||
stack.child(Label::new(format!("({code})")).color(Color::Muted))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
||||
stack.child(Label::new(format!("{source}")).color(Color::Muted))
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}
|
||||
@@ -802,11 +788,22 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
|
||||
label.into_any_element()
|
||||
} else {
|
||||
h_stack()
|
||||
.bg(gpui::red())
|
||||
.child(IconElement::new(Icon::XCircle))
|
||||
.child(Label::new(summary.error_count.to_string()))
|
||||
.child(IconElement::new(Icon::ExclamationTriangle))
|
||||
.child(Label::new(summary.warning_count.to_string()))
|
||||
.gap_1()
|
||||
.when(summary.error_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(IconElement::new(Icon::XCircle).color(Color::Error))
|
||||
.child(Label::new(summary.error_count.to_string())),
|
||||
)
|
||||
})
|
||||
.when(summary.warning_count > 0, |then| {
|
||||
then.child(
|
||||
h_stack()
|
||||
.child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning))
|
||||
.child(Label::new(summary.warning_count.to_string())),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -1550,7 +1547,7 @@ mod tests {
|
||||
block_id: ix,
|
||||
editor_style: &editor::EditorStyle::default(),
|
||||
})
|
||||
.element_id()?
|
||||
.inner_id()?
|
||||
.try_into()
|
||||
.ok()?,
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ pub struct DiagnosticIndicator {
|
||||
}
|
||||
|
||||
impl Render for DiagnosticIndicator {
|
||||
type Output = Stateful<Div>;
|
||||
type Element = Stateful<Div>;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Output {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
|
||||
(0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)),
|
||||
(0, warning_count) => h_stack()
|
||||
@@ -44,7 +44,7 @@ impl Render for DiagnosticIndicator {
|
||||
};
|
||||
|
||||
h_stack()
|
||||
.id(cx.entity_id())
|
||||
.id("diagnostic-indicator")
|
||||
.on_action(cx.listener(Self::go_to_next_diagnostic))
|
||||
.rounded_md()
|
||||
.flex_none()
|
||||
@@ -77,13 +77,13 @@ impl DiagnosticIndicator {
|
||||
|
||||
project::Event::DiskBasedDiagnosticsFinished { language_server_id }
|
||||
| project::Event::LanguageServerRemoved(language_server_id) => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
this.in_progress_checks.remove(language_server_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
project::Event::DiagnosticsUpdated { .. } => {
|
||||
this.summary = project.read(cx).diagnostic_summary(cx);
|
||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ impl DiagnosticIndicator {
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
summary: project.read(cx).diagnostic_summary(cx),
|
||||
summary: project.read(cx).diagnostic_summary(false, cx),
|
||||
in_progress_checks: project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
|
||||
use ui::prelude::*;
|
||||
use ui::{Icon, IconButton, Tooltip};
|
||||
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
|
||||
@@ -8,9 +9,9 @@ pub struct ToolbarControls {
|
||||
}
|
||||
|
||||
impl Render for ToolbarControls {
|
||||
type Output = Div;
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Output {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let include_warnings = self
|
||||
.editor
|
||||
.as_ref()
|
||||
|
||||