Compare commits
946 Commits
collab-v0.
...
v0.77.1-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d0731c724 | ||
|
|
7bbdfa1b9f | ||
|
|
6c1c079b44 | ||
|
|
1096720b41 | ||
|
|
5c7c4dd4dd | ||
|
|
da35202bbf | ||
|
|
f5c4a2a0dd | ||
|
|
77a63c6598 | ||
|
|
edd925f77b | ||
|
|
6d0f8290a4 | ||
|
|
6497ca8ccb | ||
|
|
e60dea7049 | ||
|
|
e64fe6d660 | ||
|
|
f6f09e8661 | ||
|
|
ef7d8f46df | ||
|
|
7df2440757 | ||
|
|
6fd4e28813 | ||
|
|
bca1acf6d3 | ||
|
|
097a768725 | ||
|
|
404dd43c30 | ||
|
|
c6f27903cc | ||
|
|
f6b0c56a47 | ||
|
|
e45d680126 | ||
|
|
e993d32900 | ||
|
|
09911d43bc | ||
|
|
c8696149b8 | ||
|
|
db73d831c7 | ||
|
|
d2411a6c86 | ||
|
|
726c8eb43f | ||
|
|
c59dafab7e | ||
|
|
e272a1a18f | ||
|
|
90bca1b94a | ||
|
|
06ad3a7f7b | ||
|
|
c18f1b6246 | ||
|
|
46efb844af | ||
|
|
29f0078084 | ||
|
|
432aeeac56 | ||
|
|
8440a98850 | ||
|
|
bccc34c61a | ||
|
|
e8b3d4e0fa | ||
|
|
ff1c7db38f | ||
|
|
30a08467b0 | ||
|
|
c8de738972 | ||
|
|
87ac409e51 | ||
|
|
badfe70a93 | ||
|
|
11d8394af2 | ||
|
|
c24194156e | ||
|
|
ece2af1e22 | ||
|
|
adf94a1681 | ||
|
|
09d306df85 | ||
|
|
0a5cf4b831 | ||
|
|
9398de6a57 | ||
|
|
e45104a1c0 | ||
|
|
74b10e4ba5 | ||
|
|
ddbffd2c41 | ||
|
|
00a38e4c3b | ||
|
|
37d01c7fb3 | ||
|
|
281ff92236 | ||
|
|
bb721a08f5 | ||
|
|
f50b51bdad | ||
|
|
693172854c | ||
|
|
b3c7526fb5 | ||
|
|
6e37ff880f | ||
|
|
f08685f65f | ||
|
|
ce828d55d5 | ||
|
|
f28806d09b | ||
|
|
686f5439ad | ||
|
|
b402f27d50 | ||
|
|
d39c761de5 | ||
|
|
7a600e7a65 | ||
|
|
221bb54e48 | ||
|
|
431e11a033 | ||
|
|
8b7273e46e | ||
|
|
9dc608dc4b | ||
|
|
648f0e5b7b | ||
|
|
b40ea4df14 | ||
|
|
01e3173ed0 | ||
|
|
8ee25be7b9 | ||
|
|
0384456e7d | ||
|
|
20064b5629 | ||
|
|
daed75096e | ||
|
|
718052bb72 | ||
|
|
4eb75f058f | ||
|
|
6c68a3e709 | ||
|
|
e7af3f223a | ||
|
|
baff428de5 | ||
|
|
3952e98320 | ||
|
|
51be0efa1f | ||
|
|
5bfd5e35b3 | ||
|
|
d53c18cc57 | ||
|
|
5b7d0ee6fe | ||
|
|
e2bdd261a1 | ||
|
|
9187863d0e | ||
|
|
3daeabc1d6 | ||
|
|
bebfe53e89 | ||
|
|
9328bb0153 | ||
|
|
89c283ecf0 | ||
|
|
a00ce3f286 | ||
|
|
4ce51c8138 | ||
|
|
f626920af1 | ||
|
|
325827699e | ||
|
|
a3b1980a5e | ||
|
|
709c101834 | ||
|
|
943ea61452 | ||
|
|
981b3a459f | ||
|
|
a65dd0fd98 | ||
|
|
cf6ea6d698 | ||
|
|
152755b043 | ||
|
|
dad66eb3fb | ||
|
|
f62e0b502a | ||
|
|
344f59adf7 | ||
|
|
cc33f83e4e | ||
|
|
9842b7ad1a | ||
|
|
14497027d4 | ||
|
|
ae510c80db | ||
|
|
ad7e49ed06 | ||
|
|
b687aec9d9 | ||
|
|
a435dc1339 | ||
|
|
b4561b848d | ||
|
|
baa9e271d5 | ||
|
|
350ddf2025 | ||
|
|
3594243644 | ||
|
|
904993dfc9 | ||
|
|
4179ed66a6 | ||
|
|
d173b1d412 | ||
|
|
ab4b3293d1 | ||
|
|
5892f16602 | ||
|
|
84aefb9dcb | ||
|
|
4e81513af1 | ||
|
|
90296667b0 | ||
|
|
e0f9b2b40f | ||
|
|
477453c396 | ||
|
|
19fc143209 | ||
|
|
ca03d871a6 | ||
|
|
0a3f0c5252 | ||
|
|
c80942ea00 | ||
|
|
caa6a75238 | ||
|
|
4f4af55329 | ||
|
|
1e5aff9e51 | ||
|
|
ee154feda4 | ||
|
|
3b31f10c6f | ||
|
|
8db7e17ac5 | ||
|
|
1f6bd0ea77 | ||
|
|
ba652fc033 | ||
|
|
7163ba429b | ||
|
|
c832e4406e | ||
|
|
515724821e | ||
|
|
0867162c87 | ||
|
|
aba2914a31 | ||
|
|
246a6adab7 | ||
|
|
020a0965b0 | ||
|
|
b74553455f | ||
|
|
4a8527478d | ||
|
|
4c179875ab | ||
|
|
f89f33347d | ||
|
|
9dee2ca2be | ||
|
|
62aeb6b8b3 | ||
|
|
5210be95fe | ||
|
|
7d7053b990 | ||
|
|
118435a348 | ||
|
|
86e2101592 | ||
|
|
50586812ec | ||
|
|
416c793076 | ||
|
|
a0637a769c | ||
|
|
9401ef223d | ||
|
|
620890c411 | ||
|
|
0ec984f924 | ||
|
|
01bbf20962 | ||
|
|
996294ba67 | ||
|
|
ddf2f2cb0a | ||
|
|
bd4d7551a5 | ||
|
|
5097cf5cb7 | ||
|
|
13212d274e | ||
|
|
b9110c9268 | ||
|
|
b9573872e1 | ||
|
|
3ec71a742d | ||
|
|
50682dc685 | ||
|
|
2bca64f13b | ||
|
|
606d683f29 | ||
|
|
ff2e6bc3bd | ||
|
|
218f2fd0fe | ||
|
|
bb0257bbac | ||
|
|
929ebd7175 | ||
|
|
124aa74b03 | ||
|
|
a2f75eb031 | ||
|
|
6194c5df16 | ||
|
|
d14b684237 | ||
|
|
7a8cba0544 | ||
|
|
f1b5bf051a | ||
|
|
ad4201f768 | ||
|
|
75a9cfdabe | ||
|
|
3b6f66791f | ||
|
|
9311e01271 | ||
|
|
6d068e926b | ||
|
|
6854063d0b | ||
|
|
7ca0b38048 | ||
|
|
a598f0b13c | ||
|
|
eb6088701e | ||
|
|
24ba47e75d | ||
|
|
3dd5b3f426 | ||
|
|
9f86ca8574 | ||
|
|
bc2ea58c6a | ||
|
|
b343e8056a | ||
|
|
6a2a1303c4 | ||
|
|
a366ba19af | ||
|
|
70cb2fa8d7 | ||
|
|
f67c3f1f1d | ||
|
|
bde0456111 | ||
|
|
8734bd8435 | ||
|
|
34fbffb4cc | ||
|
|
368d2a73ea | ||
|
|
e7b56f6342 | ||
|
|
1deff43639 | ||
|
|
a890b8f3b7 | ||
|
|
7faa0da5c7 | ||
|
|
ff85bc6d42 | ||
|
|
b00e467ede | ||
|
|
2e1adb0724 | ||
|
|
269df10a16 | ||
|
|
8358efbd6c | ||
|
|
dc11d2726e | ||
|
|
41d3c5287b | ||
|
|
2198c295b3 | ||
|
|
6cf62a5b02 | ||
|
|
f8401394f5 | ||
|
|
b53d1eef71 | ||
|
|
c397fd9a71 | ||
|
|
9b8adecf05 | ||
|
|
e0f553c0f5 | ||
|
|
37a2ef9d41 | ||
|
|
89b93d4f6f | ||
|
|
2036fc48b5 | ||
|
|
cb3e873a67 | ||
|
|
da78abd99f | ||
|
|
637e8ada42 | ||
|
|
e3061066c9 | ||
|
|
514da604d7 | ||
|
|
b9811e48e4 | ||
|
|
fb69611568 | ||
|
|
a8a045e8bf | ||
|
|
59bd503696 | ||
|
|
fb7818f93c | ||
|
|
3fb426e8b2 | ||
|
|
f0a31f86c7 | ||
|
|
dc7fe72f18 | ||
|
|
b3dffeaf2a | ||
|
|
81cbefec22 | ||
|
|
4f9a07cffc | ||
|
|
184f37015a | ||
|
|
c9997a81a3 | ||
|
|
df798c1a7f | ||
|
|
465fcec36d | ||
|
|
40c2409b80 | ||
|
|
46dc347a1a | ||
|
|
f84046b74f | ||
|
|
8c51a62a8d | ||
|
|
794e6e22a6 | ||
|
|
504d88d56c | ||
|
|
94c76c45e6 | ||
|
|
f2d6a03dff | ||
|
|
3b19a409f8 | ||
|
|
7854f4a1ef | ||
|
|
6cb35536b3 | ||
|
|
161373710c | ||
|
|
11e2caff15 | ||
|
|
36ada13966 | ||
|
|
2c61eeb56d | ||
|
|
ccae9448d4 | ||
|
|
bb46b26494 | ||
|
|
098e6969f7 | ||
|
|
d910eed1f1 | ||
|
|
64b07dbfeb | ||
|
|
4f307c7601 | ||
|
|
23c967418a | ||
|
|
77ed437cda | ||
|
|
0b1334b8c5 | ||
|
|
cdc6566d87 | ||
|
|
36f3d3d738 | ||
|
|
27712c25ef | ||
|
|
68af726ee4 | ||
|
|
0ea7959ba4 | ||
|
|
bcb7b80517 | ||
|
|
10a30cf330 | ||
|
|
06a86162bb | ||
|
|
b986c38a31 | ||
|
|
69fd273367 | ||
|
|
8e828947fb | ||
|
|
2d8adf4c56 | ||
|
|
0b48e238f2 | ||
|
|
04495aa8cd | ||
|
|
5fea49e639 | ||
|
|
0704d9dcdb | ||
|
|
a57fcf5afc | ||
|
|
e910fd8493 | ||
|
|
d5123bc832 | ||
|
|
8656708de4 | ||
|
|
72197802a2 | ||
|
|
f8f1a3f86e | ||
|
|
2ec25bef84 | ||
|
|
89ddf14b0e | ||
|
|
be86cb35ba | ||
|
|
465d8cc2ff | ||
|
|
93b9e762ec | ||
|
|
fbc934b884 | ||
|
|
350b7b82f7 | ||
|
|
b179fc2b99 | ||
|
|
8860346324 | ||
|
|
9004640586 | ||
|
|
03498314fa | ||
|
|
ce4b672a14 | ||
|
|
3f9405f8f1 | ||
|
|
2276d25bdf | ||
|
|
ffe53bed87 | ||
|
|
37f910949d | ||
|
|
1e3b4f0387 | ||
|
|
e1df85e86d | ||
|
|
f6601f64e5 | ||
|
|
6ccc90327c | ||
|
|
bbeb33bc7e | ||
|
|
e74db2d180 | ||
|
|
74e0bed38f | ||
|
|
832549f1a3 | ||
|
|
b965333325 | ||
|
|
2be0283bf2 | ||
|
|
59a66190e5 | ||
|
|
9334267bd0 | ||
|
|
a0daf47134 | ||
|
|
9a729a2e64 | ||
|
|
1c636500de | ||
|
|
65a9ac449f | ||
|
|
bf5c3d963a | ||
|
|
c33d0f940a | ||
|
|
24e0a027ee | ||
|
|
d49e35f947 | ||
|
|
40aee8d7bc | ||
|
|
d33d27faa4 | ||
|
|
46ead28971 | ||
|
|
111aff29cc | ||
|
|
e2a2e40599 | ||
|
|
b73423daaa | ||
|
|
0324ca3b08 | ||
|
|
36040cd0e1 | ||
|
|
a07867d628 | ||
|
|
812145f9ab | ||
|
|
dbe5b0205c | ||
|
|
3d6c81584f | ||
|
|
81ece4fd44 | ||
|
|
2ec5c88f98 | ||
|
|
7b559176f1 | ||
|
|
d7305077bf | ||
|
|
4798b72cb8 | ||
|
|
71d8ead318 | ||
|
|
9b92a8e3fe | ||
|
|
7f4da80386 | ||
|
|
6a731233c5 | ||
|
|
b7cf426908 | ||
|
|
0dc92bec5c | ||
|
|
c75aca25b6 | ||
|
|
ae87961a77 | ||
|
|
e9464815e0 | ||
|
|
1ed47663ef | ||
|
|
dd02bc7748 | ||
|
|
e403b868b7 | ||
|
|
3105ecd0bd | ||
|
|
05e9615507 | ||
|
|
1abb7794cb | ||
|
|
50e681bbb1 | ||
|
|
3fb8395085 | ||
|
|
4513c40993 | ||
|
|
4ffc8cd9fd | ||
|
|
33c265d3cf | ||
|
|
58c41778e7 | ||
|
|
2592ec7265 | ||
|
|
d6462c611c | ||
|
|
28786a3c18 | ||
|
|
a5fd0250ab | ||
|
|
f68eda97fb | ||
|
|
99236f1875 | ||
|
|
bf8658067f | ||
|
|
c697c1a96a | ||
|
|
2b6aa3f5d1 | ||
|
|
e96d52f35a | ||
|
|
ed2f1ddd2d | ||
|
|
8dd249a7cd | ||
|
|
24fcad3fa2 | ||
|
|
46af9a90ce | ||
|
|
1c69e289b7 | ||
|
|
9d782be4c8 | ||
|
|
cae9e733a1 | ||
|
|
77c396a0ab | ||
|
|
b500ed3171 | ||
|
|
6b6e4e3bfe | ||
|
|
1683a54698 | ||
|
|
14488619a3 | ||
|
|
cf4e719484 | ||
|
|
8c3232bb9b | ||
|
|
ebf1da1de8 | ||
|
|
3564e95f27 | ||
|
|
ecf77a510a | ||
|
|
927f7b3363 | ||
|
|
07bb42898f | ||
|
|
a11165ad0a | ||
|
|
ab82e13167 | ||
|
|
0e0170712e | ||
|
|
8be844a13f | ||
|
|
7c98395e77 | ||
|
|
8922156923 | ||
|
|
bda37ffb9c | ||
|
|
2982a98d1c | ||
|
|
010eba509c | ||
|
|
56b7eb6b6f | ||
|
|
6551742c58 | ||
|
|
4bb986b3be | ||
|
|
efafd1d8d3 | ||
|
|
0981244797 | ||
|
|
159d3ab00c | ||
|
|
3fb6e31b92 | ||
|
|
04df00b221 | ||
|
|
dc6f7fd577 | ||
|
|
ac3e8f61ef | ||
|
|
fc811d14b1 | ||
|
|
cdf64b6cad | ||
|
|
3a7cfc3901 | ||
|
|
5e4d113308 | ||
|
|
de6eb00e2b | ||
|
|
76975c29a9 | ||
|
|
57a7ff9a6f | ||
|
|
eebce28b32 | ||
|
|
31dac39e34 | ||
|
|
5cfe206433 | ||
|
|
ff2fb06b2c | ||
|
|
a5ad2f544e | ||
|
|
7b291df21f | ||
|
|
6e33f33da1 | ||
|
|
4ea7a24b93 | ||
|
|
48b76f96fc | ||
|
|
c72a50e203 | ||
|
|
43f61ab413 | ||
|
|
cf83ecccbb | ||
|
|
848c6b78d5 | ||
|
|
b90fc046ca | ||
|
|
98b51634c4 | ||
|
|
28eb69e74e | ||
|
|
b03eebeb6c | ||
|
|
eac33d732e | ||
|
|
2d39358323 | ||
|
|
a4a179763a | ||
|
|
19b686ad65 | ||
|
|
ac882c7db5 | ||
|
|
baee6d0342 | ||
|
|
50ccf16de1 | ||
|
|
bef2013c7f | ||
|
|
2c904cb0bf | ||
|
|
33306846a6 | ||
|
|
30caeeaeb5 | ||
|
|
0ba051a754 | ||
|
|
32191e318e | ||
|
|
7037842bef | ||
|
|
8bd20d8c3a | ||
|
|
df1775326c | ||
|
|
df0715e7c9 | ||
|
|
e56dfd9177 | ||
|
|
afb375f909 | ||
|
|
bcf7a32284 | ||
|
|
5fbc9736e5 | ||
|
|
fbd23986e3 | ||
|
|
114eef8592 | ||
|
|
5df50e2fc9 | ||
|
|
7a667f390b | ||
|
|
2482a1a9ce | ||
|
|
7641965326 | ||
|
|
8db131a3a1 | ||
|
|
4f1e8c953e | ||
|
|
c2de0f6b5e | ||
|
|
17e8172dc3 | ||
|
|
8e9d95fefc | ||
|
|
3a7ac9c0ff | ||
|
|
88c6b890bc | ||
|
|
1012cea4af | ||
|
|
4a2b7e4820 | ||
|
|
6c0b35acb0 | ||
|
|
888fcb5b1b | ||
|
|
015b8db1c3 | ||
|
|
ebe1fa7a96 | ||
|
|
7be868e372 | ||
|
|
087d51634d | ||
|
|
ea663f3017 | ||
|
|
5041300b52 | ||
|
|
2c9199fd32 | ||
|
|
327932ba3b | ||
|
|
459060764a | ||
|
|
3d53336916 | ||
|
|
c1812ddc27 | ||
|
|
d80dba1fe3 | ||
|
|
6703264600 | ||
|
|
5ce147a2ad | ||
|
|
a32c0d1c9b | ||
|
|
e65c0810ba | ||
|
|
1fcfa5d272 | ||
|
|
addfcdc1f4 | ||
|
|
4501a5a7ee | ||
|
|
a120996f0d | ||
|
|
0a50d271b7 | ||
|
|
01a590a1fb | ||
|
|
d42d495cb0 | ||
|
|
187fac1579 | ||
|
|
0acb820f04 | ||
|
|
dda0febf39 | ||
|
|
9143790602 | ||
|
|
b31813fad3 | ||
|
|
0e238210bb | ||
|
|
912c396b37 | ||
|
|
436ab6e454 | ||
|
|
889b15683d | ||
|
|
135dcf19a2 | ||
|
|
5d23aaacc8 | ||
|
|
a789476c95 | ||
|
|
da5a6a8b4f | ||
|
|
76685406ed | ||
|
|
70eedbb48e | ||
|
|
42b5fa1fa3 | ||
|
|
7de04abdcb | ||
|
|
373e88e9fb | ||
|
|
c3a88857f9 | ||
|
|
f787f6054b | ||
|
|
6f342bb2c6 | ||
|
|
0ba44c6dc4 | ||
|
|
2ff82732b9 | ||
|
|
cbfdfa8124 | ||
|
|
57e10ce7c6 | ||
|
|
a9c2f42f70 | ||
|
|
83f9d51dee | ||
|
|
767d2f9766 | ||
|
|
3fb14d7caf | ||
|
|
952cdb4e98 | ||
|
|
bbe8297297 | ||
|
|
76c066baee | ||
|
|
654ee48feb | ||
|
|
ef16963772 | ||
|
|
37c052f53d | ||
|
|
582f5d0114 | ||
|
|
fd016b9bcd | ||
|
|
317eb7535c | ||
|
|
55589533e2 | ||
|
|
9a8585ce0c | ||
|
|
aa0a18968a | ||
|
|
0777f459ba | ||
|
|
2732cc2cbe | ||
|
|
e8dad56af9 | ||
|
|
87cf8ac60e | ||
|
|
f44658ad2a | ||
|
|
20377ea4e9 | ||
|
|
db2aaa4367 | ||
|
|
099b79910f | ||
|
|
fe25994fb3 | ||
|
|
7cef4a5d40 | ||
|
|
0c49030ade | ||
|
|
e15ffc8560 | ||
|
|
58987275fc | ||
|
|
9bff82f161 | ||
|
|
be0241bab1 | ||
|
|
de0b136be2 | ||
|
|
4e80ae13ec | ||
|
|
b020955ac4 | ||
|
|
a606058537 | ||
|
|
f065399799 | ||
|
|
926b59b15d | ||
|
|
2d6219ebe2 | ||
|
|
8228618b9e | ||
|
|
d4d9a142fc | ||
|
|
035901127a | ||
|
|
37bfeed2e6 | ||
|
|
4642817e72 | ||
|
|
83e21387af | ||
|
|
3e92e4d110 | ||
|
|
303216291b | ||
|
|
8be9d21340 | ||
|
|
9742bd7fd4 | ||
|
|
3014cc5299 | ||
|
|
d6b728409f | ||
|
|
433f284571 | ||
|
|
7270f950b8 | ||
|
|
ae15673dfd | ||
|
|
8697f81a37 | ||
|
|
21ded7639a | ||
|
|
3f95788d45 | ||
|
|
1afd6f859d | ||
|
|
2b0592da21 | ||
|
|
8f61134e7e | ||
|
|
888145ebed | ||
|
|
a50f0181fb | ||
|
|
62d32db66c | ||
|
|
d6962d957b | ||
|
|
fd2a9b3df9 | ||
|
|
460dc62888 | ||
|
|
e35db69dbd | ||
|
|
a89cc22af4 | ||
|
|
e682e2dd72 | ||
|
|
65641b1d3e | ||
|
|
248161aa63 | ||
|
|
d9278f7416 | ||
|
|
57781fd7aa | ||
|
|
2d889f59bf | ||
|
|
2802e3a1c6 | ||
|
|
ea39983f78 | ||
|
|
ca2e0256e1 | ||
|
|
070b89243f | ||
|
|
e530406d62 | ||
|
|
ea0dd8972f | ||
|
|
a1308d20ce | ||
|
|
486b3f64d1 | ||
|
|
0f93386071 | ||
|
|
77a4f907a0 | ||
|
|
d6acea525d | ||
|
|
89a5506f43 | ||
|
|
5431488a9a | ||
|
|
ac7618da17 | ||
|
|
647d9861b1 | ||
|
|
d7ac15fa71 | ||
|
|
3a1d533c01 | ||
|
|
c44acaefff | ||
|
|
1593b1e13d | ||
|
|
fabcdb909a | ||
|
|
f99e4043c4 | ||
|
|
1b45911857 | ||
|
|
4918ad5789 | ||
|
|
9f86748aff | ||
|
|
489be5e77b | ||
|
|
b396e153d1 | ||
|
|
1c572fd86e | ||
|
|
73af155dd6 | ||
|
|
eca6115e4b | ||
|
|
74aeec360d | ||
|
|
2f26fcd889 | ||
|
|
a4d9d6c750 | ||
|
|
a2a3ebc42f | ||
|
|
ddf4e1a316 | ||
|
|
a369fb8033 | ||
|
|
9ff34bcb6a | ||
|
|
10f130ee30 | ||
|
|
3819a67185 | ||
|
|
6e7101ca6b | ||
|
|
2df2d09e3c | ||
|
|
4c3244b982 | ||
|
|
a79b4e312b | ||
|
|
5eac797a93 | ||
|
|
a581d0c5b8 | ||
|
|
15799f7af6 | ||
|
|
81ed961659 | ||
|
|
9db55b3029 | ||
|
|
328b779185 | ||
|
|
7f3d937938 | ||
|
|
f68f9f37ab | ||
|
|
c22d13286d | ||
|
|
44c7f162b6 | ||
|
|
7003a475a7 | ||
|
|
3d8dbee76a | ||
|
|
160870c9de | ||
|
|
ba6ffd8256 | ||
|
|
ecb7d1072f | ||
|
|
38b83a70aa | ||
|
|
1fc6276eab | ||
|
|
45e4e3354e | ||
|
|
27a80a1c94 | ||
|
|
426aeb7c5e | ||
|
|
35524db136 | ||
|
|
e928c1c61e | ||
|
|
5d4eb2b7ae | ||
|
|
db978fcb6c | ||
|
|
3329b2bbd6 | ||
|
|
a66a0cfd70 | ||
|
|
27ee994e17 | ||
|
|
0414723a54 | ||
|
|
588419492a | ||
|
|
52296836fe | ||
|
|
678ee26c5e | ||
|
|
29d67452e0 | ||
|
|
51984f0d39 | ||
|
|
4d73d4b1b9 | ||
|
|
e8cea130a4 | ||
|
|
dff08d3cfe | ||
|
|
c48e3f3d05 | ||
|
|
f3509824e8 | ||
|
|
14c72cac58 | ||
|
|
f95bda64ba | ||
|
|
96ffe84edb | ||
|
|
2b3d09f70a | ||
|
|
8e8f66a5e1 | ||
|
|
c9299a49e1 | ||
|
|
9f048a4b1c | ||
|
|
0f0d5d5726 | ||
|
|
d060114f00 | ||
|
|
9d58032064 | ||
|
|
4609be20de | ||
|
|
4d05d61ed7 | ||
|
|
8dabdd1baa | ||
|
|
4678f6e0a5 | ||
|
|
95b259b841 | ||
|
|
79cf6fb8b6 | ||
|
|
cb610f37f2 | ||
|
|
36e4dcef16 | ||
|
|
c49dc8d6e5 | ||
|
|
f086fa3f21 | ||
|
|
c118f9aabd | ||
|
|
f2a5a4d0fd | ||
|
|
fb2278dc6d | ||
|
|
50d37e1ae7 | ||
|
|
8dcaa81aad | ||
|
|
e1a58e9381 | ||
|
|
56080771e6 | ||
|
|
bb24f1142f | ||
|
|
94b2f8e07f | ||
|
|
310d867aab | ||
|
|
9f74d6e4ac | ||
|
|
f7ceebfce3 | ||
|
|
083986dfae | ||
|
|
df1e1295e3 | ||
|
|
c1934d6232 | ||
|
|
4bee273511 | ||
|
|
2e37c0ea4a | ||
|
|
2f42af2ac3 | ||
|
|
be2c601176 | ||
|
|
8dcef46842 | ||
|
|
2aa7a9e95b | ||
|
|
8af1294ba5 | ||
|
|
5a00729fad | ||
|
|
97203e1e02 | ||
|
|
95e661a78c | ||
|
|
b54b77b9ec | ||
|
|
467e3dc50a | ||
|
|
131f3471fc | ||
|
|
88170df7f0 | ||
|
|
2967b46a17 | ||
|
|
4eeb1aec50 | ||
|
|
1851e2e77c | ||
|
|
4a46227909 | ||
|
|
86371d9f5e | ||
|
|
38476f5429 | ||
|
|
6c9422808a | ||
|
|
d30e129d63 | ||
|
|
ad1947fa50 | ||
|
|
f088de5947 | ||
|
|
c85ad96b45 | ||
|
|
1f649e52de | ||
|
|
0a7111d216 | ||
|
|
a58b39f884 | ||
|
|
c124caeb0d | ||
|
|
5ce065ac92 | ||
|
|
5189dea3d5 | ||
|
|
d9948bf772 | ||
|
|
062e7a03a9 | ||
|
|
17b4bfdf98 | ||
|
|
06c31a0daa | ||
|
|
203f569f2e | ||
|
|
b0fb5913b6 | ||
|
|
6cc84a77c8 | ||
|
|
27a6951403 | ||
|
|
9f3c8c1e3a | ||
|
|
a8f466b422 | ||
|
|
f8d092fdc6 | ||
|
|
8ca0f9ac99 | ||
|
|
a653e87658 | ||
|
|
bec03dc882 | ||
|
|
2c3c8b4cb0 | ||
|
|
a0a50cb412 | ||
|
|
cf193154e1 | ||
|
|
c3518cefe8 | ||
|
|
4746fb5936 | ||
|
|
8651320c9f | ||
|
|
c9a306b4ac | ||
|
|
292708573f | ||
|
|
c3b102f5a8 | ||
|
|
f61b870db6 | ||
|
|
1a6a807db5 | ||
|
|
01aac0de48 | ||
|
|
dc88a67f50 | ||
|
|
5ce0472a75 | ||
|
|
cc788dc5f7 | ||
|
|
7726a9ec3d | ||
|
|
fcf97ab41e | ||
|
|
bb200aa082 | ||
|
|
2cd9db1cfe | ||
|
|
467e5691b9 | ||
|
|
0bd6f9b6ce | ||
|
|
244f259331 | ||
|
|
625151806a | ||
|
|
6810490bf4 | ||
|
|
3312a06368 | ||
|
|
373902d933 | ||
|
|
f62d13de21 | ||
|
|
df2e9625b3 | ||
|
|
765773cfe6 | ||
|
|
9e5612348c | ||
|
|
aa9710f7c3 | ||
|
|
b90e1012bf | ||
|
|
96186a3dae | ||
|
|
2c1fd7b0bf | ||
|
|
9779663c6b | ||
|
|
8e02266d07 | ||
|
|
24ef80f4b6 | ||
|
|
febf992a43 | ||
|
|
e9fdb13cb5 | ||
|
|
216b1aec08 | ||
|
|
02f6928328 | ||
|
|
fe27f135c0 | ||
|
|
74f8b493b2 | ||
|
|
49379924cb | ||
|
|
14eec66e38 | ||
|
|
048da9ddce | ||
|
|
9c627e82a0 | ||
|
|
14899d867e | ||
|
|
db831c3fbb | ||
|
|
bfb43c67f8 | ||
|
|
a3da41bfad | ||
|
|
ef987cae6b | ||
|
|
41ff42ddec | ||
|
|
37a4de1a84 | ||
|
|
551dc1f318 | ||
|
|
866f0e1344 | ||
|
|
a222821dfa | ||
|
|
d49a29d793 | ||
|
|
176738d674 | ||
|
|
ebbe6e7aa9 | ||
|
|
d237bdaa9b | ||
|
|
5517e743e1 | ||
|
|
c1e61b479c | ||
|
|
828f406b4f | ||
|
|
e743f3b1d8 | ||
|
|
69e28d04b0 | ||
|
|
2be4f41964 | ||
|
|
97ed89a797 | ||
|
|
ad7eaca443 | ||
|
|
ddbf251b5f | ||
|
|
95098e4f29 | ||
|
|
529ccbda3a | ||
|
|
a73e264c3d | ||
|
|
0200fc5542 | ||
|
|
9694771752 | ||
|
|
9fc7f54631 | ||
|
|
1545b2ac61 | ||
|
|
318a0b7ed0 | ||
|
|
a46ca32356 | ||
|
|
12cd712b53 | ||
|
|
3cffee4065 | ||
|
|
213658f1e9 | ||
|
|
6b337914d7 | ||
|
|
386f7ba16d | ||
|
|
5387695ee0 | ||
|
|
73e7967a12 | ||
|
|
9d4cf2ff62 | ||
|
|
83c98ce049 | ||
|
|
658541ec9f | ||
|
|
6a57bd2794 | ||
|
|
8487ae77e7 | ||
|
|
b762d70202 | ||
|
|
53cb3a4429 | ||
|
|
ef192a902a | ||
|
|
585c23e9f6 | ||
|
|
4708f5d88f | ||
|
|
a165cd596b | ||
|
|
0d31c8c1c8 | ||
|
|
8c5a0ca3a4 | ||
|
|
5c05b7d413 | ||
|
|
3da69117ae | ||
|
|
4256a96051 | ||
|
|
82e9f736bd | ||
|
|
fa620bf98f | ||
|
|
378f0c32fe | ||
|
|
404f59090c | ||
|
|
eb02834582 | ||
|
|
77e322cb75 | ||
|
|
f669b8a029 | ||
|
|
09d57d1f26 | ||
|
|
7a629769b7 | ||
|
|
bd223f5a1f | ||
|
|
1006ada458 | ||
|
|
79f8f08caf | ||
|
|
789bbf15b7 | ||
|
|
1dd085fc92 | ||
|
|
1e18480808 | ||
|
|
93a634991b | ||
|
|
90fb9b53ad | ||
|
|
d0ce7b3516 | ||
|
|
b94c265240 | ||
|
|
8d70a22fa3 | ||
|
|
a6ffcdd0cf | ||
|
|
74843493f4 | ||
|
|
6b62ce2aaa | ||
|
|
2b1118f597 | ||
|
|
233b28a1b9 | ||
|
|
eeb21af841 | ||
|
|
a5bccecd48 | ||
|
|
0f818f2458 | ||
|
|
7187cc8a4c | ||
|
|
2bc36600d4 | ||
|
|
60f29410ca | ||
|
|
ca3c4566dd | ||
|
|
f3dee2d332 | ||
|
|
273988b8d5 | ||
|
|
b6337f59fd | ||
|
|
21a0df406f | ||
|
|
599acf0daa | ||
|
|
6458a9144e | ||
|
|
344d05045d | ||
|
|
75803d8dbb | ||
|
|
04e053a216 | ||
|
|
41bff3947c | ||
|
|
46152c6249 | ||
|
|
f65fda2fa4 | ||
|
|
96ac650465 | ||
|
|
ea16082a42 | ||
|
|
42e74e7eef | ||
|
|
738e161bc6 | ||
|
|
559e14799c | ||
|
|
eeb5b03d63 | ||
|
|
d750b02a7c | ||
|
|
c321f5d94a | ||
|
|
89da738fae | ||
|
|
8cd94060bb | ||
|
|
d8ccdff9fc | ||
|
|
47348542ef | ||
|
|
b5fb8e6b8b | ||
|
|
b0336cd27e | ||
|
|
ecd80c553c | ||
|
|
59d7f06c57 | ||
|
|
15f666a50a | ||
|
|
ec6f2a3ad4 | ||
|
|
213be3d6bd | ||
|
|
55800fc696 | ||
|
|
6a2066af6c | ||
|
|
cb8962691a | ||
|
|
bb00134f5f | ||
|
|
21d6665c37 | ||
|
|
6542b30d1f | ||
|
|
55ebfe8321 | ||
|
|
9d15b3d295 | ||
|
|
d31fd9bbf2 | ||
|
|
52babc51a0 | ||
|
|
1a3940a12e | ||
|
|
1aec691b35 | ||
|
|
70dd586be9 | ||
|
|
af85db9ea5 | ||
|
|
67b265b3d5 | ||
|
|
0ede89d82a | ||
|
|
d8219545c9 | ||
|
|
06f6d02579 |
@@ -8,4 +8,4 @@ crates/collab/static/styles.css
|
||||
vendor/bin
|
||||
assets/themes/*.json
|
||||
assets/themes/internal/*.json
|
||||
assets/themes/experiments/*.json
|
||||
assets/themes/staff/*.json
|
||||
|
||||
2
.github/pull_request_template.md
vendored
@@ -1,6 +1,6 @@
|
||||
## Description of feature or change
|
||||
|
||||
## Link to related issues from zed or insiders
|
||||
## Link to related issues from zed or community
|
||||
|
||||
## Before Merging
|
||||
|
||||
|
||||
35
.github/workflows/ci.yml
vendored
@@ -4,7 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "v*"
|
||||
- "v[0-9]+.[0-9]+.x"
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
@@ -17,6 +17,26 @@ env:
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: Check formatting
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: cargo fmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
tests:
|
||||
name: Run tests
|
||||
runs-on:
|
||||
@@ -41,16 +61,22 @@ jobs:
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
|
||||
- name: Run check
|
||||
run: cargo check --workspace
|
||||
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
|
||||
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
runs-on:
|
||||
@@ -109,6 +135,9 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Generate license file
|
||||
run: script/generate-licenses
|
||||
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
|
||||
8
.github/workflows/release_actions.yml
vendored
@@ -13,12 +13,12 @@ jobs:
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
|
||||
Restart your Zed or head to https://zed.dev/releases/latest to grab it.
|
||||
|
||||
|
||||
```md
|
||||
# Changelog
|
||||
|
||||
|
||||
${{ github.event.release.body }}
|
||||
```
|
||||
mixpanel_release:
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
architecture: "x64"
|
||||
cache: "pip"
|
||||
- run: pip install -r script/mixpanel_release/requirements.txt
|
||||
- run: >
|
||||
- run: >
|
||||
python script/mixpanel_release/main.py
|
||||
${{ github.event.release.tag_name }}
|
||||
${{ secrets.MIXPANEL_PROJECT_ID }}
|
||||
|
||||
4
.gitignore
vendored
@@ -7,8 +7,8 @@
|
||||
/crates/collab/static/styles.css
|
||||
/vendor/bin
|
||||
/assets/themes/*.json
|
||||
/assets/themes/Internal/*.json
|
||||
/assets/themes/Experiments/*.json
|
||||
/assets/*licenses.md
|
||||
/assets/themes/staff/*.json
|
||||
**/venv
|
||||
.build
|
||||
Packages
|
||||
|
||||
289
Cargo.lock
generated
@@ -259,6 +259,21 @@ dependencies = [
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-global-executor"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "1.12.0"
|
||||
@@ -350,6 +365,32 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-std"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"crossbeam-utils 0.8.14",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-lite",
|
||||
"gloo-timers",
|
||||
"kv-log-macro",
|
||||
"log",
|
||||
"memchr",
|
||||
"once_cell",
|
||||
"pin-project-lite 0.2.9",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
"wasm-bindgen-futures",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-stream"
|
||||
version = "0.3.3"
|
||||
@@ -371,6 +412,20 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-tar"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c49359998a76e32ef6e870dbc079ebad8f1e53e8441c5dd39d27b44493fe331"
|
||||
dependencies = [
|
||||
"async-std",
|
||||
"filetime",
|
||||
"libc",
|
||||
"pin-project",
|
||||
"redox_syscall",
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.0.3"
|
||||
@@ -739,8 +794,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "bromberg_sl2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ed88064f69518b7e3ea50ecfc1b61d43f19248618a377b95ae5c8b611134d4d"
|
||||
source = "git+https://github.com/zed-industries/bromberg_sl2?rev=950bc5482c216c395049ae33ae4501e08975f17f#950bc5482c216c395049ae33ae4501e08975f17f"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"lazy_static",
|
||||
@@ -820,13 +874,16 @@ dependencies = [
|
||||
"async-broadcast",
|
||||
"client",
|
||||
"collections",
|
||||
"fs",
|
||||
"futures 0.3.25",
|
||||
"gpui",
|
||||
"language",
|
||||
"live_kit_client",
|
||||
"log",
|
||||
"media",
|
||||
"postage",
|
||||
"project",
|
||||
"settings",
|
||||
"util",
|
||||
]
|
||||
|
||||
@@ -1131,7 +1188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.4.2"
|
||||
version = "0.7.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-tungstenite",
|
||||
@@ -1195,11 +1252,14 @@ name = "collab_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_update",
|
||||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"context_menu",
|
||||
"editor",
|
||||
"feedback",
|
||||
"futures 0.3.25",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -1274,6 +1334,7 @@ source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2f
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"uuid 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1898,6 +1959,7 @@ dependencies = [
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript 0.20.2",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
@@ -2020,6 +2082,33 @@ dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "feedback"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"editor",
|
||||
"futures 0.3.25",
|
||||
"gpui",
|
||||
"human_bytes",
|
||||
"isahc",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"postage",
|
||||
"project",
|
||||
"search",
|
||||
"serde",
|
||||
"settings",
|
||||
"sysinfo",
|
||||
"theme",
|
||||
"tree-sitter-markdown",
|
||||
"urlencoding",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-per-thread-logger"
|
||||
version = "0.1.5"
|
||||
@@ -2050,6 +2139,18 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"windows-sys 0.42.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
@@ -2498,6 +2599,18 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gloo-timers"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "go_to_line"
|
||||
version = "0.1.0"
|
||||
@@ -2561,9 +2674,9 @@ dependencies = [
|
||||
"sum_tree",
|
||||
"time 0.3.17",
|
||||
"tiny-skia",
|
||||
"tree-sitter",
|
||||
"usvg",
|
||||
"util",
|
||||
"uuid 1.2.2",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
@@ -2757,6 +2870,12 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "human_bytes"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39b528196c838e8b3da8b665e08c30958a6f2ede91d79f2ffcd0d4664b9c64eb"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
@@ -2900,6 +3019,17 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
|
||||
|
||||
[[package]]
|
||||
name = "install_cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"log",
|
||||
"smol",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
@@ -3039,6 +3169,7 @@ dependencies = [
|
||||
name = "journal"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"dirs 4.0.0",
|
||||
"editor",
|
||||
@@ -3108,6 +3239,15 @@ dependencies = [
|
||||
"arrayvec 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kv-log-macro"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language"
|
||||
version = "0.1.0"
|
||||
@@ -3125,6 +3265,7 @@ dependencies = [
|
||||
"fuzzy",
|
||||
"git",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"lsp",
|
||||
@@ -3147,14 +3288,32 @@ dependencies = [
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-json 0.19.0",
|
||||
"tree-sitter-markdown",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-ruby",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-typescript 0.20.1",
|
||||
"unicase",
|
||||
"unindent",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
"settings",
|
||||
"theme",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@@ -3755,6 +3914,15 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
|
||||
dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@@ -4424,7 +4592,7 @@ source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"ntapi",
|
||||
"ntapi 0.3.7",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
@@ -4435,6 +4603,7 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"backtrace",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
@@ -4452,6 +4621,7 @@ dependencies = [
|
||||
"lsp",
|
||||
"parking_lot 0.11.2",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"pulldown-cmark",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
@@ -5494,6 +5664,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"editor",
|
||||
"futures 0.3.25",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
@@ -5504,6 +5675,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"theme",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -5967,6 +6139,7 @@ dependencies = [
|
||||
"parking_lot 0.11.2",
|
||||
"smol",
|
||||
"thread_local",
|
||||
"uuid 1.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6219,6 +6392,21 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1620f9573034c573376acc550f3b9a2be96daeb08abb3c12c8523e1cee06e80f"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"ntapi 0.4.0",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-interface"
|
||||
version = "0.20.0"
|
||||
@@ -6401,6 +6589,7 @@ dependencies = [
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
@@ -6847,7 +7036,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.20.9"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -6949,6 +7138,16 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-lua"
|
||||
version = "0.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d489873fd1a2fa6d5f04930bfc5c081c96f0c038c1437104518b5b842c69b282"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-markdown"
|
||||
version = "0.0.1"
|
||||
@@ -7025,6 +7224,24 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-typescript"
|
||||
version = "0.20.2"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259#5d20856f34315b068c41edaee2ac8a100081d259"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-yaml"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=9050a4a4a847ed29e25485b1292a36eab8ae3492#9050a4a4a847ed29e25485b1292a36eab8ae3492"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "try-lock"
|
||||
version = "0.2.3"
|
||||
@@ -7201,6 +7418,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urlencoding"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.14.1"
|
||||
@@ -7256,6 +7479,12 @@ dependencies = [
|
||||
"tempdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
@@ -7813,6 +8042,26 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
|
||||
|
||||
[[package]]
|
||||
name = "welcome"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"db",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"install_cli",
|
||||
"log",
|
||||
"picker",
|
||||
"project",
|
||||
"settings",
|
||||
"theme",
|
||||
"theme_selector",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wepoll-ffi"
|
||||
version = "0.1.2"
|
||||
@@ -8088,6 +8337,7 @@ dependencies = [
|
||||
"futures 0.3.25",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"install_cli",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
@@ -8099,8 +8349,10 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"terminal",
|
||||
"theme",
|
||||
"util",
|
||||
"uuid 1.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8113,6 +8365,15 @@ dependencies = [
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.4"
|
||||
@@ -8148,13 +8409,14 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.68.0"
|
||||
version = "0.77.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
"assets",
|
||||
"async-compression",
|
||||
"async-recursion 0.3.2",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"auto_update",
|
||||
"backtrace",
|
||||
@@ -8169,10 +8431,12 @@ dependencies = [
|
||||
"command_palette",
|
||||
"context_menu",
|
||||
"ctor",
|
||||
"db",
|
||||
"diagnostics",
|
||||
"easy-parallel",
|
||||
"editor",
|
||||
"env_logger",
|
||||
"feedback",
|
||||
"file_finder",
|
||||
"fs",
|
||||
"fsevent",
|
||||
@@ -8183,9 +8447,11 @@ dependencies = [
|
||||
"ignore",
|
||||
"image",
|
||||
"indexmap",
|
||||
"install_cli",
|
||||
"isahc",
|
||||
"journal",
|
||||
"language",
|
||||
"language_selector",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -8231,6 +8497,7 @@ dependencies = [
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-json 0.20.0",
|
||||
"tree-sitter-lua",
|
||||
"tree-sitter-markdown",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-racket",
|
||||
@@ -8238,11 +8505,15 @@ dependencies = [
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-scheme",
|
||||
"tree-sitter-toml",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-typescript 0.20.2",
|
||||
"tree-sitter-yaml",
|
||||
"unindent",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"util",
|
||||
"uuid 1.2.2",
|
||||
"vim",
|
||||
"welcome",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ members = [
|
||||
"crates/diagnostics",
|
||||
"crates/drag_and_drop",
|
||||
"crates/editor",
|
||||
"crates/feedback",
|
||||
"crates/file_finder",
|
||||
"crates/fs",
|
||||
"crates/fsevent",
|
||||
@@ -25,8 +26,10 @@ members = [
|
||||
"crates/go_to_line",
|
||||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/install_cli",
|
||||
"crates/journal",
|
||||
"crates/language",
|
||||
"crates/language_selector",
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
@@ -57,6 +60,7 @@ members = [
|
||||
"crates/util",
|
||||
"crates/vim",
|
||||
"crates/workspace",
|
||||
"crates/welcome",
|
||||
"crates/zed",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
@@ -68,7 +72,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
rand = { version = "0.8" }
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
|
||||
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
|
||||
@@ -83,5 +87,3 @@ split-debuginfo = "unpacked"
|
||||
|
||||
[profile.release]
|
||||
debug = true
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ WORKDIR app
|
||||
COPY . .
|
||||
|
||||
# Compile collab server
|
||||
ARG CARGO_PROFILE_RELEASE_PANIC=abort
|
||||
RUN --mount=type=cache,target=./script/node_modules \
|
||||
--mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=./target \
|
||||
|
||||
91
README.md
@@ -23,10 +23,18 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
git clone https://github.com/zed-industries/zed.dev
|
||||
```
|
||||
|
||||
* Set up a local `zed` database and seed it with some initial users:
|
||||
* Initialize submodules
|
||||
|
||||
```
|
||||
script/bootstrap
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
* Set up a local `zed` database and seed it with some initial users:
|
||||
|
||||
Create a personal GitHub token to run `script/bootstrap` once successfully. Then delete that token.
|
||||
|
||||
```
|
||||
GITHUB_TOKEN=<$token> script/bootstrap
|
||||
```
|
||||
|
||||
### Testing against locally-running servers
|
||||
@@ -49,30 +57,14 @@ script/zed-with-local-servers --release
|
||||
|
||||
If you trigger `cmd-alt-i`, Zed will copy a JSON representation of the current window contents to the clipboard. You can paste this in a tool like [DJSON](https://chrome.google.com/webstore/detail/djson-json-viewer-formatt/chaeijjekipecdajnijdldjjipaegdjc?hl=en) to navigate the state of on-screen elements in a structured way.
|
||||
|
||||
### Staff Only Features
|
||||
### Licensing
|
||||
|
||||
Many features (e.g. the terminal) take significant time and effort before they are polished enough to be released to even Alpha users. But Zed's team workflow relies on fast, daily PRs and there can be large merge conflicts for feature branchs that diverge for a few days. To bridge this gap, there is a `staff_mode` field in the Settings that staff can set to enable these unpolished or incomplete features. Note that this setting isn't leaked via autocompletion, but there is no mechanism to stop users from setting this anyway. As initilization of Zed components is only done once, on startup, setting `staff_mode` may require a restart to take effect. You can set staff only key bindings in the `assets/keymaps/internal.json` file, and add staff only themes in the `styles/src/themes/internal` directory
|
||||
We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
|
||||
|
||||
### Experimental Features
|
||||
- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
|
||||
- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
|
||||
- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
|
||||
|
||||
A user facing feature flag can be added to Zed by:
|
||||
|
||||
* Adding a setting to the crates/settings/src/settings.rs FeatureFlags struct. Use a boolean for a simple on/off, or use a struct to experiment with different configuration options.
|
||||
* If the feature needs keybindings, add a file to the `assets/keymaps/experiments/` folder, then update the `FeatureFlags::keymap_files()` method to check for your feature's flag and add it's keybindings's path to the method's list.
|
||||
* If you want to add an experimental theme, add it to the `styles/src/themes/experiments` folder
|
||||
|
||||
The Settings global should be initialized with the user's feature flags by the time the feature's `init(cx)` equivalent is called.
|
||||
|
||||
To promote an experimental feature to a full feature:
|
||||
|
||||
* If this is an experimental theme, move the theme file from the `styles/src/themes/experiments` folder to the `styles/src/themes/` folder
|
||||
* Take the features settings (if any) and add them under a new variable in the Settings struct. Don't forget to add a `merge()` call in `set_user_settings()`!
|
||||
* Take the feature's keybindings and add them to the default.json (or equivalent) file
|
||||
* Remove the file from the `FeatureFlags::keymap_files()` method
|
||||
* Remove the conditional in the feature's `init(cx)` equivalent.
|
||||
|
||||
|
||||
That's it 😸
|
||||
|
||||
### Wasm Plugins
|
||||
|
||||
@@ -83,56 +75,3 @@ rustup target add wasm32-wasi
|
||||
```
|
||||
|
||||
Plugins can be found in the `plugins` folder in the root. For more information about how plugins work, check the [Plugin Guide](./crates/plugin_runtime/README.md) in `crates/plugin_runtime/README.md`.
|
||||
|
||||
## Roadmap
|
||||
|
||||
We will organize our efforts around the following major milestones. We'll create tracking issues for each of these milestones to detail the individual tasks that comprise them.
|
||||
|
||||
### Minimal text editor
|
||||
|
||||
[Tracking issue](https://github.com/zed-industries/zed/issues/2)
|
||||
|
||||
Ship a minimal text editor to investors and other insiders. It should be extremely fast and stable, but all it can do is open, edit, and save text files, making it potentially useful for basic editing but not for real coding.
|
||||
|
||||
Establish basic infrastructure for building the app bundle and uploading an artifact. Once this is released, we should regularly distribute updates as features land.
|
||||
|
||||
### Collaborative code editor for internal use
|
||||
|
||||
[Tracking issue](https://github.com/zed-industries/zed/issues/6)
|
||||
|
||||
Turn the minimal text editor into a collaborative _code_ editor. This will include the minimal features that the Zed team needs to collaborate in Zed to build Zed without net loss in developer productivity. This includes productivity-critical features such as:
|
||||
|
||||
- Syntax highlighting and syntax-aware editing and navigation
|
||||
- The ability to see and edit non-local working copies of a repository
|
||||
- Language server support for Rust code navigation, refactoring, diagnostics, etc.
|
||||
- Project browsing and project-wide search and replace
|
||||
|
||||
We want to tackle collaboration fairly early so that the rest of the design of the product can flow around that assumption. We could probably produce a single-player code editor more quickly, but at the risk of having collaboration feel more "bolted on" when we eventually add it.
|
||||
|
||||
### Private alpha for Rust teams on macOS
|
||||
|
||||
The "minimal" milestones were about getting Zed to a point where the Zed team could use Zed productively to build Zed. What features are required for someone outside the company to use Zed to productively work on another project that is also written in Rust?
|
||||
|
||||
This includes infrastructure like auto-updates, error reporting, and metrics collection. It also includes some amount of polish to make the tool more discoverable for someone that didn't write it, such as a UI for updating settings and key bindings. We may also need to enhance the server to support user authentication and related concerns.
|
||||
|
||||
The initial target audience is like us. A small team working in Rust that's potentially interested in collaborating. As the alpha proceeds, we can work with teams of different sizes.
|
||||
|
||||
### Private beta for Rust teams on macOS
|
||||
|
||||
Once we're getting sufficiently positive feedback from our initial alpha users, we widen the audience by letting people share invites. Now may be a good time to get Zed running on the web, so that it's extremely easy for a Zed user to share a link and be collaborating in seconds. Once someone is using Zed on the Web, we'll let them register for the private beta and download the native binary if they're on macOS.
|
||||
|
||||
### Expand to other languages
|
||||
|
||||
Depending on how the Rust beta is going, focus hard on dominating another niche language such as Elixr or getting a foothold within a niche of a larger language, such as React/Typescript. Alternatively, go wide at this point and add decent support several widely-used languages such as Python, Ruby, Typescript, etc. This would entail taking 1-2 weeks per language and making sure we ship a solid experience based on a publicly-available language server. Each language has slightly different development practices, so we need to make sure Zed's UX meshes well with those practices.
|
||||
|
||||
### Future directions
|
||||
|
||||
Each of these sections could probably broken into multiple milestones, but this part of the roadmap is too far in the future to go into that level of detail at this point.
|
||||
|
||||
#### Expand to other platforms
|
||||
|
||||
Support Linux and Windows. We'll probably want to hire at least one person that prefers to work on each respective platform and have them spearhead the effort to port Zed to that platform. Once they've done so, they can join the general development effort while ensuring the user experience stays good on that platform.
|
||||
|
||||
#### Expand on collaboration
|
||||
|
||||
To start with, we'll focus on synchronous collaboration because that's where we're most differentiated, but there's no reason we have to limit ourselves to that. How can our tool facilitate collaboration generally, whether it's sync or async? What would it take for a team to go 100% Zed and collaborate fully within the tool? If we haven't added it already, basic Git support would be nice.
|
||||
|
||||
3
assets/icons/ellipsis_14.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="4" viewBox="0 0 14 4" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.125 2C3.125 2.62132 2.62132 3.125 2 3.125C1.37868 3.125 0.875 2.62132 0.875 2C0.875 1.37868 1.37868 0.875 2 0.875C2.62132 0.875 3.125 1.37868 3.125 2ZM8.125 2C8.125 2.62132 7.62132 3.125 7 3.125C6.37868 3.125 5.875 2.62132 5.875 2C5.875 1.37868 6.37868 0.875 7 0.875C7.62132 0.875 8.125 1.37868 8.125 2ZM12 3.125C12.6213 3.125 13.125 2.62132 13.125 2C13.125 1.37868 12.6213 0.875 12 0.875C11.3787 0.875 10.875 1.37868 10.875 2C10.875 2.62132 11.3787 3.125 12 3.125Z" fill="#ABB2BF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
3
assets/icons/leave_12.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 1C0 0.585786 0.335786 0.25 0.75 0.25H7.25C7.66421 0.25 8 0.585786 8 1C8 1.41421 7.66421 1.75 7.25 1.75H1.5V10.25H7.25C7.66421 10.25 8 10.5858 8 11C8 11.4142 7.66421 11.75 7.25 11.75H0.75C0.335786 11.75 0 11.4142 0 11V1ZM8.78148 2.91435C9.10493 2.65559 9.57689 2.70803 9.83565 3.03148L11.8357 5.53148C12.0548 5.80539 12.0548 6.19461 11.8357 6.46852L9.83565 8.96852C9.57689 9.29197 9.10493 9.34441 8.78148 9.08565C8.45803 8.82689 8.40559 8.35493 8.66435 8.03148L9.68953 6.75H3.75C3.33579 6.75 3 6.41421 3 6C3 5.58579 3.33579 5.25 3.75 5.25H9.68953L8.66435 3.96852C8.40559 3.64507 8.45803 3.17311 8.78148 2.91435Z" fill="#ABB2BF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 784 B |
3
assets/icons/logo_96.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 715 B |
3
assets/icons/speech_bubble_12.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.6667 0.400196H1.33346C0.819658 0.400196 0.399658 0.820196 0.399658 1.3326V10.6658C0.399658 11.181 0.816998 11.5982 1.33206 11.5982C1.58966 11.5982 1.82206 11.4932 1.99146 11.3238L4.51706 8.79684H10.6639C11.1763 8.79684 11.5963 8.37544 11.5963 7.86304V1.3298C11.5963 0.815996 11.1749 0.395996 10.6625 0.395996L10.6667 0.400196ZM2.2667 2.2664H6.00008V3.1988H2.26628V2.265L2.2667 2.2664ZM7.8667 6.93316H2.2667V5.99936H7.8667V6.93176V6.93316ZM9.7329 5.06556H2.26488V4.13176H9.73164V5.06416L9.7329 5.06556Z" fill="#282C34"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 636 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.2 6.00001C5.52563 6.00001 6.6 4.92545 6.6 3.60001C6.6 2.27457 5.52563 1.20001 4.2 1.20001C2.87438 1.20001 1.8 2.27457 1.8 3.60001C1.8 4.92545 2.87438 6.00001 4.2 6.00001ZM5.15063 6.90001H3.24938C1.45519 6.90001 0 8.35501 0 10.1494C0 10.5094 0.291 10.8 0.649875 10.8H7.7505C8.10938 10.8 8.4 10.5094 8.4 10.1494C8.4 8.35501 6.945 6.90001 5.15063 6.90001ZM11.55 4.95001H10.65V4.05001C10.65 3.80251 10.4494 3.60001 10.2 3.60001C9.95063 3.60001 9.75 3.80157 9.75 4.05001V4.95001H8.85C8.6025 4.95001 8.4 5.15251 8.4 5.40001C8.4 5.64751 8.60156 5.85001 8.85 5.85001H9.75V6.75001C9.75 6.99939 9.9525 7.20001 10.2 7.20001C10.4475 7.20001 10.65 6.99845 10.65 6.75001V5.85001H11.55C11.7994 5.85001 12 5.64939 12 5.40001C12 5.15064 11.7994 4.95001 11.55 4.95001Z" fill="white"/>
|
||||
<path d="M5.75062 7.09998H3.24938C1.45519 7.09998 0 8.55498 0 10.3493C0 10.7093 0.291 11 0.649875 11H8.3505C8.70938 11 9 10.7093 9 10.3493C9 8.55498 7.545 7.09998 5.75062 7.09998Z" fill="white"/>
|
||||
<path d="M7 3.5C7 4.82544 5.82562 6 4.5 6C3.17438 6 2 4.82544 2 3.5C2 2.17456 3.17438 1 4.5 1C5.82562 1 7 2.17456 7 3.5Z" fill="white"/>
|
||||
<path d="M9.5 3.75V5.5M9.5 7.25V5.5M9.5 5.5H11.25M9.5 5.5H7.75" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 882 B After Width: | Height: | Size: 564 B |
@@ -1,3 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.9 8.00002C7.44656 8.00002 8.7 6.74637 8.7 5.20002C8.7 3.65368 7.44656 2.40002 5.9 2.40002C4.35344 2.40002 3.1 3.65368 3.1 5.20002C3.1 6.74637 4.35344 8.00002 5.9 8.00002ZM7.00906 9.05002H4.79094C2.69772 9.05002 1 10.7475 1 12.841C1 13.261 1.3395 13.6 1.75819 13.6H10.0422C10.4609 13.6 10.8 13.261 10.8 12.841C10.8 10.7475 9.1025 9.05002 7.00906 9.05002ZM14.475 6.77502H13.425V5.72502C13.425 5.43627 13.1909 5.20002 12.9 5.20002C12.6091 5.20002 12.375 5.43518 12.375 5.72502V6.77502H11.325C11.0363 6.77502 10.8 7.01127 10.8 7.30002C10.8 7.58877 11.0352 7.82502 11.325 7.82502H12.375V8.87502C12.375 9.16596 12.6112 9.40002 12.9 9.40002C13.1887 9.40002 13.425 9.16487 13.425 8.87502V7.82502H14.475C14.7659 7.82502 15 7.59096 15 7.30002C15 7.00909 14.7659 6.77502 14.475 6.77502Z" fill="white"/>
|
||||
<path d="M7.00906 8.99999H4.79094C2.69772 8.99999 1 11.1475 1 13.2409C1 13.6609 1.3395 14 1.75819 14H10.0422C10.4609 14 10.8 13.6609 10.8 13.2409C10.8 11.1475 9.1025 8.99999 7.00906 8.99999Z" fill="white"/>
|
||||
<path d="M9 5C9 6.54634 7.44657 7.99998 5.90001 7.99998C4.35344 7.99998 3 6.54634 3 5C3 3.45366 4.45344 2 6 2C7.54656 2 9 3.45366 9 5Z" fill="white"/>
|
||||
<path d="M13.025 6H14.475C14.7659 6 15 6.20906 15 6.5C15 6.79094 14.7659 7 14.475 7H13V8.49995C13 8.7898 12.7638 9.02495 12.475 9.02495C12.1863 9.02495 11.95 8.79089 11.95 8.49995V7H10.525C10.2352 7 10 6.78875 10 6.5C10 6.21125 10.2362 6 10.525 6H11.975V4.525C11.975 4.23516 12.2091 4 12.5 4C12.7909 4 13.025 4.23625 13.025 4.525V6Z" fill="white"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 907 B After Width: | Height: | Size: 810 B |
68
assets/keymaps/atom.json
Normal file
@@ -0,0 +1,68 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-k cmd-p": "workspace::ActivatePreviousPane",
|
||||
"cmd-k cmd-n": "workspace::ActivateNextPane"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-g": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"cmd-f3": "search::SelectNextMatch",
|
||||
"cmd-shift-f3": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-\\": "workspace::ToggleLeftSidebar",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-r": "project_symbols::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"alt-cmd-/": "search::ToggleRegex",
|
||||
"ctrl-0": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||
"h": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-]": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-shift-c": "project_panel::CopyPath"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
@@ -20,8 +20,10 @@
|
||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||
"alt-cmd-right": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"cmd-shift-w": "workspace::CloseWindow",
|
||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||
"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",
|
||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||
@@ -36,7 +38,7 @@
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open",
|
||||
"alt-cmd-o": "recent_projects::Toggle",
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"ctrl-`": "workspace::NewTerminal"
|
||||
}
|
||||
},
|
||||
@@ -67,9 +69,11 @@
|
||||
"up": "editor::MoveUp",
|
||||
"pageup": "editor::PageUp",
|
||||
"shift-pageup": "editor::MovePageUp",
|
||||
"home": "editor::MoveToBeginningOfLine",
|
||||
"down": "editor::MoveDown",
|
||||
"pagedown": "editor::PageDown",
|
||||
"shift-pagedown": "editor::MovePageDown",
|
||||
"end": "editor::MoveToEndOfLine",
|
||||
"left": "editor::MoveLeft",
|
||||
"right": "editor::MoveRight",
|
||||
"ctrl-p": "editor::MoveUp",
|
||||
@@ -110,6 +114,12 @@
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"shift-home": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-a": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
@@ -122,6 +132,12 @@
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"shift-end": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-e": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
@@ -148,6 +164,7 @@
|
||||
"bindings": {
|
||||
"enter": "editor::Newline",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"alt-z": "editor::ToggleSoftWrap",
|
||||
"cmd-f": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
@@ -170,10 +187,10 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"context": "BufferSearchBar > Editor",
|
||||
"bindings": {
|
||||
"escape": "buffer_search::Dismiss",
|
||||
"cmd-f": "buffer_search::FocusEditor",
|
||||
"tab": "buffer_search::FocusEditor",
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPrevMatch"
|
||||
}
|
||||
@@ -211,7 +228,13 @@
|
||||
"replace_newest": true
|
||||
}
|
||||
],
|
||||
"cmd-/": "editor::ToggleComments",
|
||||
"cmd-k cmd-i": "editor::Hover",
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": false
|
||||
}
|
||||
],
|
||||
"alt-up": "editor::SelectLargerSyntaxNode",
|
||||
"alt-down": "editor::SelectSmallerSyntaxNode",
|
||||
"cmd-u": "editor::UndoSelection",
|
||||
@@ -226,7 +249,8 @@
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"ctrl-space": "editor::ShowCompletions",
|
||||
"cmd-.": "editor::ToggleCodeActions"
|
||||
"cmd-.": "editor::ToggleCodeActions",
|
||||
"alt-cmd-r": "editor::RevealInFinder"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -330,7 +354,8 @@
|
||||
"cmd-shift-p": "command_palette::Toggle",
|
||||
"cmd-shift-m": "diagnostics::Deploy",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll"
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
@@ -396,7 +421,7 @@
|
||||
{
|
||||
"bindings": {
|
||||
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
|
||||
"cmd-shift-c": "collab::ToggleCollaborationMenu",
|
||||
"cmd-shift-c": "collab::ToggleContactsMenu",
|
||||
"cmd-alt-i": "zed::DebugElements"
|
||||
}
|
||||
},
|
||||
@@ -417,8 +442,7 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::FocusDock",
|
||||
"cmd-shift-b": "workspace::ToggleRightSidebar"
|
||||
"shift-escape": "dock::FocusDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -429,15 +453,16 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"shift-escape": "dock::HideDock"
|
||||
"cmd-escape": "dock::AddTabToDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"context": "Pane && docked",
|
||||
"bindings": {
|
||||
"cmd-escape": "dock::MoveActiveItemToDock"
|
||||
"shift-escape": "dock::HideDock",
|
||||
"cmd-escape": "dock::RemoveTabFromDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -450,7 +475,8 @@
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"cmd-alt-c": "project_panel::CopyPath",
|
||||
"f2": "project_panel::Rename",
|
||||
"backspace": "project_panel::Delete"
|
||||
"backspace": "project_panel::Delete",
|
||||
"alt-cmd-r": "project_panel::RevealInFinder"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -514,4 +540,4 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[]
|
||||
78
assets/keymaps/jetbrains.json
Normal file
@@ -0,0 +1,78 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"cmd-d": "editor::DuplicateLine",
|
||||
"cmd-pagedown": "editor::MovePageDown",
|
||||
"cmd-pageup": "editor::MovePageUp",
|
||||
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"shift-enter": "editor::NewlineBelow",
|
||||
"cmd--": "editor::Fold",
|
||||
"cmd-=": "editor::UnfoldLines",
|
||||
"alt-shift-g": "editor::SplitSelectionIntoLines",
|
||||
"ctrl-g": [
|
||||
"editor::SelectNext",
|
||||
{
|
||||
"replace_newest": false
|
||||
}
|
||||
],
|
||||
"cmd-/": [
|
||||
"editor::ToggleComments",
|
||||
{
|
||||
"advance_downwards": true
|
||||
}
|
||||
],
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"cmd-[": "pane::GoBack",
|
||||
"cmd-]": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
"cmd-alt-f7": "editor::FindAllReferences",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-alt-b": "editor::GoToDefinition",
|
||||
"cmd-shift-b": "editor::GoToTypeDefinition",
|
||||
"alt-enter": "editor::ToggleCodeActions",
|
||||
"f2": "editor::GoToDiagnostic",
|
||||
"cmd-f2": "editor::GoToPrevDiagnostic",
|
||||
"ctrl-alt-shift-down": "editor::GoToHunk",
|
||||
"ctrl-alt-shift-up": "editor::GoToPrevHunk",
|
||||
"cmd-home": "editor::MoveToBeginning",
|
||||
"cmd-end": "editor::MoveToEnd",
|
||||
"cmd-shift-home": "editor::SelectToBeginning",
|
||||
"cmd-shift-end": "editor::SelectToEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-f12": "outline::Toggle",
|
||||
"cmd-7": "outline::Toggle",
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-l": "go_to_line::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle",
|
||||
"cmd-1": "workspace::ToggleLeftSidebar",
|
||||
"cmd-6": "diagnostics::Deploy",
|
||||
"alt-f12": "dock::FocusDock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"alt-f12": "dock::HideDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
60
assets/keymaps/sublime_text.json
Normal file
@@ -0,0 +1,60 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-[": "pane::ActivatePrevItem",
|
||||
"cmd-shift-]": "pane::ActivateNextItem",
|
||||
"ctrl-pagedown": "pane::ActivatePrevItem",
|
||||
"ctrl-pageup": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivateNextItem",
|
||||
"ctrl-tab": "pane::ActivatePrevItem",
|
||||
"cmd-+": "zed::IncreaseBufferFontSize"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"cmd-shift-space": "editor::SelectAll",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
|
||||
"shift-f12": "editor::FindAllReferences",
|
||||
"alt-cmd-down": "editor::GoToDefinition",
|
||||
"alt-shift-cmd-down": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {
|
||||
"cmd-r": "outline::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"f4": "search::SelectNextMatch",
|
||||
"shift-f4": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::FocusDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"shift-cmd-r": "project_symbols::Toggle",
|
||||
// Currently busted: https://github.com/zed-industries/feedback/issues/898
|
||||
"ctrl-0": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::HideDock"
|
||||
}
|
||||
}
|
||||
]
|
||||
90
assets/keymaps/textmate.json
Normal file
@@ -0,0 +1,90 @@
|
||||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-o": "projects::OpenRecent",
|
||||
"cmd-alt-tab": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"cmd-l": "go_to_line::Toggle",
|
||||
"ctrl-shift-d": "editor::DuplicateLine",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-j": "editor::ScrollCursorCenter",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-shift-l": "editor::SelectLine",
|
||||
"cmd-shift-t": "outline::Toggle",
|
||||
"alt-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"alt-shift-backspace": "editor::DeleteToNextWordEnd",
|
||||
"alt-delete": "editor::DeleteToNextWordEnd",
|
||||
"alt-shift-delete": "editor::DeleteToNextWordEnd",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextSubwordEnd",
|
||||
"alt-left": [
|
||||
"editor::MoveToPreviousWordStart",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-right": [
|
||||
"editor::MoveToNextWordEnd",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-left": "editor::MoveToPreviousSubwordStart",
|
||||
"ctrl-right": "editor::MoveToNextSubwordEnd",
|
||||
"cmd-shift-left": "editor::SelectToBeginningOfLine",
|
||||
"cmd-shift-right": "editor::SelectToEndOfLine",
|
||||
"alt-shift-left": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"alt-shift-right": [
|
||||
"editor::SelectToEndOfLine",
|
||||
{
|
||||
"stop_at_soft_wraps": true
|
||||
}
|
||||
],
|
||||
"ctrl-shift-left": "editor::SelectToPreviousSubwordStart",
|
||||
"ctrl-shift-right": "editor::SelectToNextSubwordEnd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == full",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "BufferSearchBar",
|
||||
"bindings": {
|
||||
"ctrl-s": "search::SelectNextMatch",
|
||||
"ctrl-shift-s": "search::SelectPrevMatch"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-t": "project_symbols::Toggle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Pane",
|
||||
"bindings": {
|
||||
"alt-cmd-r": "search::ToggleRegex",
|
||||
"ctrl-tab": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
[
|
||||
{
|
||||
"context": "Editor && VimControl",
|
||||
"context": "Editor && VimControl && !VimWaiting",
|
||||
"bindings": {
|
||||
"g": [
|
||||
"vim::PushOperator",
|
||||
@@ -27,6 +27,7 @@
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"j": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"k": "vim::Up",
|
||||
"l": "vim::Right",
|
||||
"$": "vim::EndOfLine",
|
||||
@@ -53,6 +54,42 @@
|
||||
}
|
||||
],
|
||||
"%": "vim::Matching",
|
||||
"ctrl-y": [
|
||||
"vim::Scroll",
|
||||
"LineUp"
|
||||
],
|
||||
"f": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindForward": {
|
||||
"before": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"t": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindForward": {
|
||||
"before": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"shift-f": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindBackward": {
|
||||
"after": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"shift-t": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindBackward": {
|
||||
"after": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"escape": "editor::Cancel",
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"1": [
|
||||
@@ -94,7 +131,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none",
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"c": [
|
||||
"vim::PushOperator",
|
||||
@@ -174,9 +211,9 @@
|
||||
"vim::Scroll",
|
||||
"LineDown"
|
||||
],
|
||||
"ctrl-y": [
|
||||
"vim::Scroll",
|
||||
"LineUp"
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -197,7 +234,8 @@
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
],
|
||||
"d": "editor::GoToDefinition"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -255,14 +293,18 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual",
|
||||
"context": "Editor && vim_mode == visual && !VimWaiting",
|
||||
"bindings": {
|
||||
"u": "editor::Undo",
|
||||
"c": "vim::VisualChange",
|
||||
"d": "vim::VisualDelete",
|
||||
"x": "vim::VisualDelete",
|
||||
"y": "vim::VisualYank",
|
||||
"p": "vim::VisualPaste"
|
||||
"p": "vim::VisualPaste",
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -271,5 +313,13 @@
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimWaiting",
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": "editor::Cancel"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -13,16 +13,15 @@
|
||||
// Whether to show the informational hover box when moving the mouse
|
||||
// over symbols in the editor.
|
||||
"hover_popover_enabled": true,
|
||||
// Whether to confirm before quitting Zed.
|
||||
"confirm_quit": false,
|
||||
// Whether the cursor blinks in the editor.
|
||||
"cursor_blink": true,
|
||||
// Whether to pop the completions menu while typing in an editor without
|
||||
// explicitly requesting it.
|
||||
"show_completions_on_input": true,
|
||||
// Whether new projects should start out 'online'. Online projects
|
||||
// appear in the contacts panel under your name, so that your contacts
|
||||
// can see which projects you are working on. Regardless of this
|
||||
// setting, projects keep their last online status when you reopen them.
|
||||
"projects_online_by_default": true,
|
||||
// Whether the screen sharing icon is showed in the os status bar.
|
||||
"show_call_status_icon": true,
|
||||
// Whether to use language servers to provide code intelligence.
|
||||
"enable_language_server": true,
|
||||
// When to automatically save edited buffers. This setting can
|
||||
@@ -46,7 +45,13 @@
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
"default_dock_anchor": "bottom",
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
// Whether or not to ensure there's a single newline at the end of a buffer
|
||||
// when saving it.
|
||||
"ensure_final_newline_on_save": true,
|
||||
// Whether or not to perform a buffer format before saving
|
||||
"format_on_save": "on",
|
||||
// How to perform a buffer format. This setting can take two values:
|
||||
@@ -79,6 +84,15 @@
|
||||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// Control what info is collected by Zed.
|
||||
"telemetry": {
|
||||
// Send debug info like crash reports.
|
||||
"diagnostics": true,
|
||||
// Send anonymized usage data like what languages you're using Zed with.
|
||||
"metrics": true
|
||||
},
|
||||
// Automatically update Zed
|
||||
"auto_update": true,
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
@@ -183,12 +197,6 @@
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"C": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"C++": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
@@ -199,9 +207,6 @@
|
||||
"Markdown": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"Rust": {
|
||||
"tab_size": 4
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
@@ -210,6 +215,9 @@
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"YAML": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// LSP Specific settings.
|
||||
@@ -221,7 +229,7 @@
|
||||
// rust-analyzer
|
||||
// typescript-language-server
|
||||
// vscode-json-languageserver
|
||||
// "rust_analyzer": {
|
||||
// "rust-analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "activity_indicator"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/activity_indicator.rs"
|
||||
|
||||
@@ -33,6 +33,19 @@ struct LspStatus {
|
||||
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,
|
||||
action: Option<Box<dyn Action>>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ActivityIndicator::show_error_message);
|
||||
cx.add_action(ActivityIndicator::dismiss_error_message);
|
||||
@@ -69,6 +82,8 @@ impl ActivityIndicator {
|
||||
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(),
|
||||
@@ -130,7 +145,7 @@ impl ActivityIndicator {
|
||||
fn pending_language_server_work<'a>(
|
||||
&self,
|
||||
cx: &'a AppContext,
|
||||
) -> impl Iterator<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
|
||||
) -> impl Iterator<Item = PendingWork<'a>> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.language_server_statuses()
|
||||
@@ -142,23 +157,29 @@ impl ActivityIndicator {
|
||||
let mut pending_work = status
|
||||
.pending_work
|
||||
.iter()
|
||||
.map(|(token, progress)| (status.name.as_str(), token.as_str(), progress))
|
||||
.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(|(_, _, progress)| Reverse(progress.last_update_at));
|
||||
pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
|
||||
Some(pending_work)
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn content_to_render(
|
||||
&mut self,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> (Option<&'static str>, String, Option<Box<dyn Action>>) {
|
||||
fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some((lang_server_name, progress_token, progress)) = pending_work.next() {
|
||||
let mut message = lang_server_name.to_string();
|
||||
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() {
|
||||
@@ -176,7 +197,11 @@ impl ActivityIndicator {
|
||||
write!(&mut message, " + {} more", additional_work_count).unwrap();
|
||||
}
|
||||
|
||||
return (None, message, None);
|
||||
return Content {
|
||||
icon: None,
|
||||
message,
|
||||
action: None,
|
||||
};
|
||||
}
|
||||
|
||||
// Show any language server installation info.
|
||||
@@ -199,19 +224,19 @@ impl ActivityIndicator {
|
||||
}
|
||||
|
||||
if !downloading.is_empty() {
|
||||
return (
|
||||
Some(DOWNLOAD_ICON),
|
||||
format!(
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Downloading {} language server{}...",
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
None,
|
||||
);
|
||||
action: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
return (
|
||||
Some(DOWNLOAD_ICON),
|
||||
format!(
|
||||
return Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: format!(
|
||||
"Checking for updates to {} language server{}...",
|
||||
checking_for_update.join(", "),
|
||||
if checking_for_update.len() > 1 {
|
||||
@@ -220,49 +245,61 @@ impl ActivityIndicator {
|
||||
""
|
||||
}
|
||||
),
|
||||
None,
|
||||
);
|
||||
action: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
return (
|
||||
Some(WARNING_ICON),
|
||||
format!(
|
||||
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 { "" }
|
||||
),
|
||||
Some(Box::new(ShowErrorMessage)),
|
||||
);
|
||||
action: Some(Box::new(ShowErrorMessage)),
|
||||
};
|
||||
}
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Checking for Zed updates…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Downloading => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Downloading Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Installing => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
"Installing Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None),
|
||||
AutoUpdateStatus::Errored => (
|
||||
Some(WARNING_ICON),
|
||||
"Auto update failed".to_string(),
|
||||
Some(Box::new(DismissErrorMessage)),
|
||||
),
|
||||
return match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
action: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
action: Some(Box::new(workspace::Restart)),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: "Auto update failed".to_string(),
|
||||
action: Some(Box::new(DismissErrorMessage)),
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
}
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
|
||||
return Content {
|
||||
icon: None,
|
||||
message: most_recent_active_task.to_string(),
|
||||
action: None,
|
||||
};
|
||||
}
|
||||
|
||||
Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +313,11 @@ impl View for ActivityIndicator {
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let (icon, message, action) = self.content_to_render(cx);
|
||||
let Content {
|
||||
icon,
|
||||
message,
|
||||
action,
|
||||
} = self.content_to_render(cx);
|
||||
|
||||
let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "assets"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/assets.rs"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "auto_update"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/auto_update.rs"
|
||||
|
||||
@@ -2,15 +2,16 @@ mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN};
|
||||
use client::{ZED_APP_PATH, ZED_APP_VERSION};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
MutableAppContext, Task, WeakViewHandle,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use smol::{fs::File, io::AsyncReadExt, process::Command};
|
||||
use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration};
|
||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
||||
use update_notification::UpdateNotification;
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::Workspace;
|
||||
@@ -18,13 +19,6 @@ use workspace::Workspace;
|
||||
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
|
||||
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ZED_APP_VERSION: Option<AppVersion> = env::var("ZED_APP_VERSION")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok());
|
||||
pub static ref ZED_APP_PATH: Option<PathBuf> = env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
||||
}
|
||||
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
@@ -60,7 +54,23 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut Mutab
|
||||
let server_url = server_url;
|
||||
let auto_updater = cx.add_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url.clone());
|
||||
updater.start_polling(cx).detach();
|
||||
|
||||
let mut update_subscription = cx
|
||||
.global::<Settings>()
|
||||
.auto_update
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<Settings, _>(move |updater, cx| {
|
||||
if cx.global::<Settings>().auto_update {
|
||||
if update_subscription.is_none() {
|
||||
*(&mut update_subscription) = Some(updater.start_polling(cx))
|
||||
}
|
||||
} else {
|
||||
(&mut update_subscription).take();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
updater
|
||||
});
|
||||
cx.set_global(Some(auto_updater));
|
||||
|
||||
@@ -78,7 +78,7 @@ impl View for UpdateNotification {
|
||||
)
|
||||
.with_child({
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
Text::new("View the release notes".to_string(), style.text.clone())
|
||||
Text::new("View the release notes", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "breadcrumbs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/breadcrumbs.rs"
|
||||
|
||||
@@ -47,7 +47,7 @@ impl View for Breadcrumbs {
|
||||
{
|
||||
Flex::row()
|
||||
.with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
|
||||
Label::new(" 〉 ".to_string(), theme.breadcrumbs.text.clone()).boxed()
|
||||
Label::new(" 〉 ", theme.breadcrumbs.text.clone()).boxed()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(theme.breadcrumbs.container)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "call"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/call.rs"
|
||||
@@ -23,8 +24,11 @@ collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
log = "0.4"
|
||||
live_kit_client = { path = "../live_kit_client" }
|
||||
fs = { path = "../fs" }
|
||||
language = { path = "../language" }
|
||||
media = { path = "../media" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
@@ -34,6 +38,8 @@ postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
pub mod participant;
|
||||
pub mod room;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, Client, TypedEnvelope, User, UserStore};
|
||||
use collections::HashSet;
|
||||
use futures::{future::Shared, FutureExt};
|
||||
use postage::watch;
|
||||
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
|
||||
Subscription, Task, WeakModelHandle,
|
||||
};
|
||||
pub use participant::ParticipantLocation;
|
||||
use postage::watch;
|
||||
use project::Project;
|
||||
|
||||
pub use participant::ParticipantLocation;
|
||||
pub use room::Room;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut MutableAppContext) {
|
||||
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
|
||||
@@ -27,8 +31,10 @@ pub struct IncomingCall {
|
||||
pub initial_project: Option<proto::ParticipantProject>,
|
||||
}
|
||||
|
||||
/// Singleton global maintaining the user's participation in a room across workspaces.
|
||||
pub struct ActiveCall {
|
||||
room: Option<(ModelHandle<Room>, Vec<Subscription>)>,
|
||||
pending_room_creation: Option<Shared<Task<Result<ModelHandle<Room>, Arc<anyhow::Error>>>>>,
|
||||
location: Option<WeakModelHandle<Project>>,
|
||||
pending_invites: HashSet<u64>,
|
||||
incoming_call: (
|
||||
@@ -52,6 +58,7 @@ impl ActiveCall {
|
||||
) -> Self {
|
||||
Self {
|
||||
room: None,
|
||||
pending_room_creation: None,
|
||||
location: None,
|
||||
pending_invites: Default::default(),
|
||||
incoming_call: watch::channel(),
|
||||
@@ -120,45 +127,74 @@ impl ActiveCall {
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
if !self.pending_invites.insert(called_user_id) {
|
||||
return Task::ready(Err(anyhow!("user was already invited")));
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let invite = async {
|
||||
if let Some(room) = this.read_with(&cx, |this, _| this.room().cloned()) {
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.share_project(initial_project, cx)
|
||||
})
|
||||
|
||||
let room = if let Some(room) = self.room().cloned() {
|
||||
Some(Task::ready(Ok(room)).shared())
|
||||
} else {
|
||||
self.pending_room_creation.clone()
|
||||
};
|
||||
|
||||
let invite = if let Some(room) = room {
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
|
||||
let initial_project_id = if let Some(initial_project) = initial_project {
|
||||
Some(
|
||||
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await?;
|
||||
)
|
||||
} else {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(called_user_id, initial_project, client, user_store, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room), cx))
|
||||
.await?;
|
||||
None
|
||||
};
|
||||
|
||||
Ok(())
|
||||
};
|
||||
room.update(&mut cx, |room, cx| {
|
||||
room.call(called_user_id, initial_project_id, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
} else {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let room = cx
|
||||
.spawn(|this, mut cx| async move {
|
||||
let create_room = async {
|
||||
let room = cx
|
||||
.update(|cx| {
|
||||
Room::create(
|
||||
called_user_id,
|
||||
initial_project,
|
||||
client,
|
||||
user_store,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(room)
|
||||
};
|
||||
|
||||
let room = create_room.await;
|
||||
this.update(&mut cx, |this, _| this.pending_room_creation = None);
|
||||
room.map_err(Arc::new)
|
||||
})
|
||||
.shared();
|
||||
self.pending_room_creation = Some(room.clone());
|
||||
cx.foreground().spawn(async move {
|
||||
room.await.map_err(|err| anyhow!("{:?}", err))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
};
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let result = invite.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
@@ -228,12 +264,13 @@ impl ActiveCall {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
room.update(cx, |room, cx| room.leave(cx))?;
|
||||
cx.notify();
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn share_project(
|
||||
@@ -248,6 +285,18 @@ impl ActiveCall {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unshare_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_location(
|
||||
&mut self,
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
|
||||
@@ -7,18 +7,20 @@ use client::{
|
||||
proto::{self, PeerId},
|
||||
Client, TypedEnvelope, User, UserStore,
|
||||
};
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use gpui::{
|
||||
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate};
|
||||
use postage::stream::Stream;
|
||||
use project::Project;
|
||||
use std::{mem, sync::Arc, time::Duration};
|
||||
use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration};
|
||||
use util::{post_inc, ResultExt, TryFutureExt};
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = client::RECEIVE_TIMEOUT;
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
@@ -43,6 +45,8 @@ pub struct Room {
|
||||
id: u64,
|
||||
live_kit: Option<LiveKitRoom>,
|
||||
status: RoomStatus,
|
||||
shared_projects: HashSet<WeakModelHandle<Project>>,
|
||||
joined_projects: HashSet<WeakModelHandle<Project>>,
|
||||
local_participant: LocalParticipant,
|
||||
remote_participants: BTreeMap<u64, RemoteParticipant>,
|
||||
pending_participants: Vec<Arc<User>>,
|
||||
@@ -51,6 +55,7 @@ pub struct Room {
|
||||
leave_when_empty: bool,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
follows_by_leader_id_project_id: HashMap<(PeerId, u64), Vec<PeerId>>,
|
||||
subscriptions: Vec<client::Subscription>,
|
||||
pending_room_update: Option<Task<()>>,
|
||||
maintain_connection: Option<Task<Option<()>>>,
|
||||
@@ -59,10 +64,27 @@ pub struct Room {
|
||||
impl Entity for Room {
|
||||
type Event = Event;
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
if self.status.is_online() {
|
||||
log::info!("room was released, sending leave message");
|
||||
self.client.send(proto::LeaveRoom {}).log_err();
|
||||
self.leave_internal(cx).detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn app_will_quit(
|
||||
&mut self,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<Pin<Box<dyn Future<Output = ()>>>> {
|
||||
if self.status.is_online() {
|
||||
let leave = self.leave_internal(cx);
|
||||
Some(
|
||||
cx.background()
|
||||
.spawn(async move {
|
||||
leave.await.log_err();
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,6 +154,8 @@ impl Room {
|
||||
id,
|
||||
live_kit: live_kit_room,
|
||||
status: RoomStatus::Online,
|
||||
shared_projects: Default::default(),
|
||||
joined_projects: Default::default(),
|
||||
participant_user_ids: Default::default(),
|
||||
local_participant: Default::default(),
|
||||
remote_participants: Default::default(),
|
||||
@@ -142,6 +166,7 @@ impl Room {
|
||||
pending_room_update: None,
|
||||
client,
|
||||
user_store,
|
||||
follows_by_leader_id_project_id: Default::default(),
|
||||
maintain_connection: Some(maintain_connection),
|
||||
}
|
||||
}
|
||||
@@ -226,14 +251,34 @@ impl Room {
|
||||
&& self.pending_call_count == 0
|
||||
}
|
||||
|
||||
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
if self.status.is_offline() {
|
||||
return Err(anyhow!("room is offline"));
|
||||
}
|
||||
|
||||
pub(crate) fn leave(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
cx.emit(Event::Left);
|
||||
self.leave_internal(cx)
|
||||
}
|
||||
|
||||
fn leave_internal(&mut self, cx: &mut MutableAppContext) -> Task<Result<()>> {
|
||||
if self.status.is_offline() {
|
||||
return Task::ready(Err(anyhow!("room is offline")));
|
||||
}
|
||||
|
||||
log::info!("leaving room");
|
||||
|
||||
for project in self.shared_projects.drain() {
|
||||
if let Some(project) = project.upgrade(cx) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.unshare(cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
for project in self.joined_projects.drain() {
|
||||
if let Some(project) = project.upgrade(cx) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.disconnected_from_host(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
self.status = RoomStatus::Offline;
|
||||
self.remote_participants.clear();
|
||||
self.pending_participants.clear();
|
||||
@@ -242,8 +287,12 @@ impl Room {
|
||||
self.live_kit.take();
|
||||
self.pending_room_update.take();
|
||||
self.maintain_connection.take();
|
||||
self.client.send(proto::LeaveRoom {})?;
|
||||
Ok(())
|
||||
|
||||
let leave_room = self.client.request(proto::LeaveRoom {});
|
||||
cx.background().spawn(async move {
|
||||
leave_room.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
async fn maintain_connection(
|
||||
@@ -253,20 +302,17 @@ impl Room {
|
||||
) -> Result<()> {
|
||||
let mut client_status = client.status();
|
||||
loop {
|
||||
let is_connected = client_status
|
||||
.next()
|
||||
.await
|
||||
.map_or(false, |s| s.is_connected());
|
||||
let _ = client_status.try_recv();
|
||||
let is_connected = client_status.borrow().is_connected();
|
||||
// Even if we're initially connected, any future change of the status means we momentarily disconnected.
|
||||
if !is_connected || client_status.next().await.is_some() {
|
||||
log::info!("detected client disconnection");
|
||||
let room_id = this
|
||||
.upgrade(&cx)
|
||||
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.status = RoomStatus::Rejoining;
|
||||
cx.notify();
|
||||
this.id
|
||||
});
|
||||
|
||||
// Wait for client to re-establish a connection to the server.
|
||||
@@ -275,36 +321,29 @@ impl Room {
|
||||
let client_reconnection = async {
|
||||
let mut remaining_attempts = 3;
|
||||
while remaining_attempts > 0 {
|
||||
if client_status.borrow().is_connected() {
|
||||
log::info!("client reconnected, attempting to rejoin room");
|
||||
|
||||
let Some(this) = this.upgrade(&cx) else { break };
|
||||
if this
|
||||
.update(&mut cx, |this, cx| this.rejoin(cx))
|
||||
.await
|
||||
.log_err()
|
||||
.is_some()
|
||||
{
|
||||
return true;
|
||||
} else {
|
||||
remaining_attempts -= 1;
|
||||
}
|
||||
} else if client_status.borrow().is_signed_out() {
|
||||
return false;
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"waiting for client status change, remaining attempts {}",
|
||||
remaining_attempts
|
||||
);
|
||||
if let Some(status) = client_status.next().await {
|
||||
if status.is_connected() {
|
||||
log::info!("client reconnected, attempting to rejoin room");
|
||||
let rejoin_room = async {
|
||||
let response =
|
||||
client.request(proto::JoinRoom { id: room_id }).await?;
|
||||
let room_proto =
|
||||
response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.upgrade(&cx)
|
||||
.ok_or_else(|| anyhow!("room was dropped"))?
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.status = RoomStatus::Online;
|
||||
this.apply_room_update(room_proto, cx)
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
if rejoin_room.await.log_err().is_some() {
|
||||
return true;
|
||||
} else {
|
||||
remaining_attempts -= 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
client_status.next().await;
|
||||
}
|
||||
false
|
||||
}
|
||||
@@ -326,18 +365,96 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
// The client failed to re-establish a connection to the server
|
||||
// or an error occurred while trying to re-join the room. Either way
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// The client failed to re-establish a connection to the server
|
||||
// or an error occurred while trying to re-join the room. Either way
|
||||
// we leave the room and return an error.
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
log::info!("reconnection failed, leaving room");
|
||||
let _ = this.update(&mut cx, |this, cx| this.leave(cx));
|
||||
}
|
||||
Err(anyhow!(
|
||||
"can't reconnect to room: client failed to re-establish connection"
|
||||
))
|
||||
}
|
||||
|
||||
fn rejoin(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let mut projects = HashMap::default();
|
||||
let mut reshared_projects = Vec::new();
|
||||
let mut rejoined_projects = Vec::new();
|
||||
self.shared_projects.retain(|project| {
|
||||
if let Some(handle) = project.upgrade(cx) {
|
||||
let project = handle.read(cx);
|
||||
if let Some(project_id) = project.remote_id() {
|
||||
projects.insert(project_id, handle.clone());
|
||||
reshared_projects.push(proto::UpdateProject {
|
||||
project_id,
|
||||
worktrees: project.worktree_metadata_protos(cx),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
});
|
||||
self.joined_projects.retain(|project| {
|
||||
if let Some(handle) = project.upgrade(cx) {
|
||||
let project = handle.read(cx);
|
||||
if let Some(project_id) = project.remote_id() {
|
||||
projects.insert(project_id, handle.clone());
|
||||
rejoined_projects.push(proto::RejoinProject {
|
||||
id: project_id,
|
||||
worktrees: project
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
proto::RejoinWorktree {
|
||||
id: worktree.id().to_proto(),
|
||||
scan_id: worktree.completed_scan_id() as u64,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
});
|
||||
|
||||
let response = self.client.request(proto::RejoinRoom {
|
||||
id: self.id,
|
||||
reshared_projects,
|
||||
rejoined_projects,
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = response.await?;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = RoomStatus::Online;
|
||||
this.apply_room_update(room_proto, cx)?;
|
||||
|
||||
for reshared_project in response.reshared_projects {
|
||||
if let Some(project) = projects.get(&reshared_project.id) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.reshared(reshared_project, cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for rejoined_project in response.rejoined_projects {
|
||||
if let Some(project) = projects.get(&rejoined_project.id) {
|
||||
project.update(cx, |project, cx| {
|
||||
project.rejoined(rejoined_project, cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u64 {
|
||||
@@ -370,6 +487,12 @@ impl Room {
|
||||
self.participant_user_ids.contains(&user_id)
|
||||
}
|
||||
|
||||
pub fn followers_for(&self, leader_id: PeerId, project_id: u64) -> &[PeerId] {
|
||||
self.follows_by_leader_id_project_id
|
||||
.get(&(leader_id, project_id))
|
||||
.map_or(&[], |v| v.as_slice())
|
||||
}
|
||||
|
||||
async fn handle_room_updated(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::RoomUpdated>,
|
||||
@@ -400,11 +523,13 @@ impl Room {
|
||||
.iter()
|
||||
.map(|p| p.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_participant_user_ids = room
|
||||
.participants
|
||||
.iter()
|
||||
.map(|p| p.user_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (remote_participants, pending_participants) =
|
||||
self.user_store.update(cx, move |user_store, cx| {
|
||||
(
|
||||
@@ -412,6 +537,7 @@ impl Room {
|
||||
user_store.get_users(pending_participant_user_ids, cx),
|
||||
)
|
||||
});
|
||||
|
||||
self.pending_room_update = Some(cx.spawn(|this, mut cx| async move {
|
||||
let (remote_participants, pending_participants) =
|
||||
futures::join!(remote_participants, pending_participants);
|
||||
@@ -454,6 +580,20 @@ impl Room {
|
||||
}
|
||||
|
||||
for unshared_project_id in old_projects.difference(&new_projects) {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade(cx) {
|
||||
project.update(cx, |project, cx| {
|
||||
if project.remote_id() == Some(*unshared_project_id) {
|
||||
project.disconnected_from_host(cx);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
cx.emit(Event::RemoteProjectUnshared {
|
||||
project_id: *unshared_project_id,
|
||||
});
|
||||
@@ -486,7 +626,7 @@ impl Room {
|
||||
|
||||
if let Some(live_kit) = this.live_kit.as_ref() {
|
||||
let tracks =
|
||||
live_kit.room.remote_video_tracks(&peer_id.to_string());
|
||||
live_kit.room.remote_video_tracks(&user.id.to_string());
|
||||
for track in tracks {
|
||||
this.remote_video_track_updated(
|
||||
RemoteVideoTrackUpdate::Subscribed(track),
|
||||
@@ -519,6 +659,27 @@ impl Room {
|
||||
}
|
||||
}
|
||||
|
||||
this.follows_by_leader_id_project_id.clear();
|
||||
for follower in room.followers {
|
||||
let project_id = follower.project_id;
|
||||
let (leader, follower) = match (follower.leader_id, follower.follower_id) {
|
||||
(Some(leader), Some(follower)) => (leader, follower),
|
||||
|
||||
_ => {
|
||||
log::error!("Follower message {follower:?} missing some state");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let list = this
|
||||
.follows_by_leader_id_project_id
|
||||
.entry((leader, project_id))
|
||||
.or_insert(Vec::new());
|
||||
if !list.contains(&follower) {
|
||||
list.push(follower);
|
||||
}
|
||||
}
|
||||
|
||||
this.pending_room_update.take();
|
||||
if this.should_leave() {
|
||||
log::info!("room is empty, leaving");
|
||||
@@ -622,14 +783,40 @@ impl Room {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_call_count -= 1;
|
||||
if this.should_leave() {
|
||||
this.leave(cx)?;
|
||||
this.leave(cx).detach_and_log_err(cx);
|
||||
}
|
||||
result
|
||||
})?;
|
||||
});
|
||||
result?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn join_project(
|
||||
&mut self,
|
||||
id: u64,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ModelHandle<Project>>> {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let project =
|
||||
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.joined_projects.retain(|project| {
|
||||
if let Some(project) = project.upgrade(cx) {
|
||||
!project.read(cx).is_read_only()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
this.joined_projects.insert(project.downgrade());
|
||||
});
|
||||
Ok(project)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn share_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
@@ -641,31 +828,18 @@ impl Room {
|
||||
|
||||
let request = self.client.request(proto::ShareProject {
|
||||
room_id: self.id(),
|
||||
worktrees: project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
proto::WorktreeMetadata {
|
||||
id: worktree.id().to_proto(),
|
||||
root_name: worktree.root_name().into(),
|
||||
visible: worktree.is_visible(),
|
||||
abs_path: worktree.abs_path().to_string_lossy().into(),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
worktrees: project.read(cx).worktree_metadata_protos(cx),
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
project.update(&mut cx, |project, cx| {
|
||||
project
|
||||
.shared(response.project_id, cx)
|
||||
.detach_and_log_err(cx)
|
||||
});
|
||||
project.shared(response.project_id, cx)
|
||||
})?;
|
||||
|
||||
// If the user's location is in this project, it changes from UnsharedProject to SharedProject.
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.shared_projects.insert(project.downgrade());
|
||||
let active_project = this.local_participant.active_project.as_ref();
|
||||
if active_project.map_or(false, |location| *location == project) {
|
||||
this.set_location(Some(&project), cx)
|
||||
@@ -679,6 +853,20 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn unshare_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
let project_id = match project.read(cx).remote_id() {
|
||||
Some(project_id) => project_id,
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
self.client.send(proto::UnshareProject { project_id })?;
|
||||
project.update(cx, |this, cx| this.unshare(cx))
|
||||
}
|
||||
|
||||
pub(crate) fn set_location(
|
||||
&mut self,
|
||||
project: Option<&ModelHandle<Project>>,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/cli.rs"
|
||||
|
||||
@@ -9,7 +9,13 @@ use core_foundation::{
|
||||
use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
|
||||
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
|
||||
use serde::Deserialize;
|
||||
use std::{ffi::OsStr, fs, path::PathBuf, ptr};
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs::{self, OpenOptions},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
ptr,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
|
||||
@@ -54,6 +60,12 @@ fn main() -> Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for path in args.paths.iter() {
|
||||
if !path.exists() {
|
||||
touch(path.as_path())?;
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = launch_app(bundle_path)?;
|
||||
|
||||
tx.send(CliRequest::Open {
|
||||
@@ -77,6 +89,13 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn touch(path: &Path) -> io::Result<()> {
|
||||
match OpenOptions::new().create(true).write(true).open(path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn locate_bundle() -> Result<PathBuf> {
|
||||
let cli_path = std::env::current_exe()?.canonicalize()?;
|
||||
let mut app_path = cli_path.clone();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/client.rs"
|
||||
|
||||
@@ -15,7 +15,7 @@ use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamEx
|
||||
use gpui::{
|
||||
actions,
|
||||
serde_json::{self, Value},
|
||||
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
|
||||
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext, AppVersion,
|
||||
AsyncAppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use http::HttpClient;
|
||||
@@ -25,6 +25,7 @@ use postage::watch;
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings, TelemetrySettings};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
@@ -54,13 +55,18 @@ lazy_static! {
|
||||
pub static ref ADMIN_API_TOKEN: Option<String> = std::env::var("ZED_ADMIN_API_TOKEN")
|
||||
.ok()
|
||||
.and_then(|s| if s.is_empty() { None } else { Some(s) });
|
||||
pub static ref ZED_APP_VERSION: Option<AppVersion> = std::env::var("ZED_APP_VERSION")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok());
|
||||
pub static ref ZED_APP_PATH: Option<PathBuf> =
|
||||
std::env::var("ZED_APP_PATH").ok().map(PathBuf::from);
|
||||
}
|
||||
|
||||
pub const ZED_SECRET_CLIENT_TOKEN: &str = "618033988749894";
|
||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
|
||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
actions!(client, [Authenticate]);
|
||||
actions!(client, [Authenticate, SignOut]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
cx.add_global_action({
|
||||
@@ -73,6 +79,16 @@ pub fn init(client: Arc<Client>, cx: &mut MutableAppContext) {
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let client = client.clone();
|
||||
move |_: &SignOut, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
client.disconnect(&cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
@@ -163,6 +179,10 @@ impl Status {
|
||||
pub fn is_connected(&self) -> bool {
|
||||
matches!(self, Self::Connected { .. })
|
||||
}
|
||||
|
||||
pub fn is_signed_out(&self) -> bool {
|
||||
matches!(self, Self::SignedOut | Self::UpgradeRequired)
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientState {
|
||||
@@ -423,7 +443,9 @@ impl Client {
|
||||
}));
|
||||
}
|
||||
Status::SignedOut | Status::UpgradeRequired => {
|
||||
self.telemetry.set_authenticated_user_info(None, false);
|
||||
let telemetry_settings = cx.read(|cx| cx.global::<Settings>().telemetry());
|
||||
self.telemetry
|
||||
.set_authenticated_user_info(None, false, telemetry_settings);
|
||||
state._reconnect_task.take();
|
||||
}
|
||||
_ => {}
|
||||
@@ -706,7 +728,13 @@ impl Client {
|
||||
credentials = read_credentials_from_keychain(cx);
|
||||
read_from_keychain = credentials.is_some();
|
||||
if read_from_keychain {
|
||||
self.report_event("read credentials from keychain", Default::default());
|
||||
cx.read(|cx| {
|
||||
self.report_event(
|
||||
"read credentials from keychain",
|
||||
Default::default(),
|
||||
cx.global::<Settings>().telemetry(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
if credentials.is_none() {
|
||||
@@ -997,6 +1025,8 @@ impl Client {
|
||||
let executor = cx.background();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let http = self.http.clone();
|
||||
let metrics_enabled = cx.read(|cx| cx.global::<Settings>().telemetry());
|
||||
|
||||
executor.clone().spawn(async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
// zed server to encrypt the user's access token, so that it can'be intercepted by
|
||||
@@ -1079,7 +1109,11 @@ impl Client {
|
||||
.context("failed to decrypt access token")?;
|
||||
platform.activate(true);
|
||||
|
||||
telemetry.report_event("authenticate with browser", Default::default());
|
||||
telemetry.report_event(
|
||||
"authenticate with browser",
|
||||
Default::default(),
|
||||
metrics_enabled,
|
||||
);
|
||||
|
||||
Ok(Credentials {
|
||||
user_id: user_id.parse()?,
|
||||
@@ -1132,11 +1166,9 @@ impl Client {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
|
||||
let conn_id = self.connection_id()?;
|
||||
self.peer.disconnect(conn_id);
|
||||
pub fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) {
|
||||
self.peer.teardown();
|
||||
self.set_status(Status::SignedOut, cx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connection_id(&self) -> Result<ConnectionId> {
|
||||
@@ -1235,6 +1267,7 @@ impl Client {
|
||||
subscriber
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
self.peer.respond_with_unhandled_message(message).log_err();
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1278,6 +1311,7 @@ impl Client {
|
||||
.detach();
|
||||
} else {
|
||||
log::info!("unhandled message {}", type_name);
|
||||
self.peer.respond_with_unhandled_message(message).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1285,13 +1319,27 @@ impl Client {
|
||||
self.telemetry.start();
|
||||
}
|
||||
|
||||
pub fn report_event(&self, kind: &str, properties: Value) {
|
||||
self.telemetry.report_event(kind, properties.clone());
|
||||
pub fn report_event(
|
||||
&self,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
self.telemetry
|
||||
.report_event(kind, properties.clone(), telemetry_settings);
|
||||
}
|
||||
|
||||
pub fn telemetry_log_file_path(&self) -> Option<PathBuf> {
|
||||
self.telemetry.log_file_path()
|
||||
}
|
||||
|
||||
pub fn metrics_id(&self) -> Option<Arc<str>> {
|
||||
self.telemetry.metrics_id()
|
||||
}
|
||||
|
||||
pub fn is_staff(&self) -> Option<bool> {
|
||||
self.telemetry.is_staff()
|
||||
}
|
||||
}
|
||||
|
||||
impl WeakSubscriber {
|
||||
|
||||
@@ -9,7 +9,7 @@ pub use isahc::{
|
||||
Error,
|
||||
};
|
||||
use smol::future::FutureExt;
|
||||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
pub use url::Url;
|
||||
|
||||
pub type Request = isahc::Request<AsyncBody>;
|
||||
@@ -41,7 +41,13 @@ pub trait HttpClient: Send + Sync {
|
||||
}
|
||||
|
||||
pub fn client() -> Arc<dyn HttpClient> {
|
||||
Arc::new(isahc::HttpClient::builder().build().unwrap())
|
||||
Arc::new(
|
||||
isahc::HttpClient::builder()
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.low_speed_timeout(100, Duration::from_secs(5))
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
impl HttpClient for isahc::HttpClient {
|
||||
|
||||
@@ -10,6 +10,7 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use settings::TelemetrySettings;
|
||||
use std::{
|
||||
io::Write,
|
||||
mem,
|
||||
@@ -39,6 +40,7 @@ struct TelemetryState {
|
||||
next_event_id: usize,
|
||||
flush_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
}
|
||||
|
||||
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
|
||||
@@ -124,6 +126,7 @@ impl Telemetry {
|
||||
flush_task: Default::default(),
|
||||
next_event_id: 0,
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -184,16 +187,24 @@ impl Telemetry {
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// This method takes the entire TelemetrySettings struct in order to force client code
|
||||
/// to pull the struct out of the settings global. Do not remove!
|
||||
pub fn set_authenticated_user_info(
|
||||
self: &Arc<Self>,
|
||||
metrics_id: Option<String>,
|
||||
is_staff: bool,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
if !telemetry_settings.metrics() {
|
||||
return;
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
let mut state = self.state.lock();
|
||||
let device_id = state.device_id.clone();
|
||||
let metrics_id: Option<Arc<str>> = metrics_id.map(|id| id.into());
|
||||
state.metrics_id = metrics_id.clone();
|
||||
state.is_staff = Some(is_staff);
|
||||
drop(state);
|
||||
|
||||
if let Some((token, device_id)) = MIXPANEL_TOKEN.as_ref().zip(device_id) {
|
||||
@@ -213,7 +224,7 @@ impl Telemetry {
|
||||
.header("Content-Type", "application/json")
|
||||
.body(json_bytes.into())?;
|
||||
this.http_client.send(request).await?;
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
@@ -221,7 +232,16 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_event(self: &Arc<Self>, kind: &str, properties: Value) {
|
||||
pub fn report_event(
|
||||
self: &Arc<Self>,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
if !telemetry_settings.metrics() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let event = MixpanelEvent {
|
||||
event: kind.to_string(),
|
||||
@@ -261,6 +281,14 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metrics_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().metrics_id.clone()
|
||||
}
|
||||
|
||||
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
|
||||
self.state.lock().is_staff
|
||||
}
|
||||
|
||||
fn flush(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.queue);
|
||||
@@ -292,7 +320,7 @@ impl Telemetry {
|
||||
.header("Content-Type", "application/json")
|
||||
.body(json_bytes.into())?;
|
||||
this.http_client.send(request).await?;
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
|
||||
@@ -5,8 +5,9 @@ use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt};
|
||||
use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task};
|
||||
use postage::{sink::Sink, watch};
|
||||
use rpc::proto::{RequestMessage, UsersResponse};
|
||||
use settings::Settings;
|
||||
use std::sync::{Arc, Weak};
|
||||
use util::TryFutureExt as _;
|
||||
use util::{StaffMode, TryFutureExt as _};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct User {
|
||||
@@ -141,14 +142,24 @@ impl UserStore {
|
||||
let fetch_metrics_id =
|
||||
client.request(proto::GetPrivateUserInfo {}).log_err();
|
||||
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
|
||||
if let Some(info) = info {
|
||||
client.telemetry.set_authenticated_user_info(
|
||||
Some(info.metrics_id.clone()),
|
||||
info.staff,
|
||||
);
|
||||
} else {
|
||||
client.telemetry.set_authenticated_user_info(None, false);
|
||||
}
|
||||
client.telemetry.set_authenticated_user_info(
|
||||
info.as_ref().map(|info| info.metrics_id.clone()),
|
||||
info.as_ref().map(|info| info.staff).unwrap_or(false),
|
||||
cx.read(|cx| cx.global::<Settings>().telemetry()),
|
||||
);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_default_global(|staff_mode: &mut StaffMode, _| {
|
||||
if !staff_mode.0 {
|
||||
*staff_mode = StaffMode(
|
||||
info.as_ref()
|
||||
.map(|info| info.staff)
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
()
|
||||
});
|
||||
});
|
||||
|
||||
current_user_tx.send(user).await.ok();
|
||||
}
|
||||
@@ -172,6 +183,11 @@ impl UserStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
pub fn clear_cache(&mut self) {
|
||||
self.users.clear();
|
||||
}
|
||||
|
||||
async fn handle_update_invite_info(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::UpdateInviteInfo>,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "clock"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/clock.rs"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
DATABASE_URL = "postgres://postgres@localhost/zed"
|
||||
DATABASE_MAX_CONNECTIONS = 5
|
||||
HTTP_PORT = 8080
|
||||
API_TOKEN = "secret"
|
||||
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
|
||||
|
||||
@@ -3,7 +3,8 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.4.2"
|
||||
version = "0.7.2"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "collab"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ZED_ENVIRONMENT=preview
|
||||
RUST_LOG=info
|
||||
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
||||
DATABASE_MAX_CONNECTIONS=10
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ZED_ENVIRONMENT=production
|
||||
RUST_LOG=info
|
||||
INVITE_LINK_PREFIX=https://zed.dev/invites/
|
||||
DATABASE_MAX_CONNECTIONS=85
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
ZED_ENVIRONMENT=staging
|
||||
RUST_LOG=info
|
||||
INVITE_LINK_PREFIX=https://staging.zed.dev/invites/
|
||||
DATABASE_MAX_CONNECTIONS=5
|
||||
|
||||
@@ -59,6 +59,13 @@ spec:
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 5
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
@@ -73,6 +80,8 @@ spec:
|
||||
secretKeyRef:
|
||||
name: database
|
||||
key: url
|
||||
- name: DATABASE_MAX_CONNECTIONS
|
||||
value: "${DATABASE_MAX_CONNECTIONS}"
|
||||
- name: API_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
|
||||
@@ -57,7 +57,8 @@ CREATE TABLE "worktrees" (
|
||||
"abs_path" VARCHAR NOT NULL,
|
||||
"visible" BOOL NOT NULL,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"is_complete" BOOL NOT NULL,
|
||||
"is_complete" BOOL NOT NULL DEFAULT FALSE,
|
||||
"completed_scan_id" INTEGER NOT NULL,
|
||||
PRIMARY KEY(project_id, id)
|
||||
);
|
||||
CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
|
||||
@@ -65,6 +66,7 @@ CREATE INDEX "index_worktrees_on_project_id" ON "worktrees" ("project_id");
|
||||
CREATE TABLE "worktree_entries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"id" INTEGER NOT NULL,
|
||||
"is_dir" BOOL NOT NULL,
|
||||
"path" VARCHAR NOT NULL,
|
||||
@@ -73,6 +75,7 @@ CREATE TABLE "worktree_entries" (
|
||||
"mtime_nanos" INTEGER NOT NULL,
|
||||
"is_symlink" BOOL NOT NULL,
|
||||
"is_ignored" BOOL NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE
|
||||
);
|
||||
@@ -140,3 +143,17 @@ CREATE TABLE "servers" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"environment" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE "followers" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"leader_connection_id" INTEGER NOT NULL,
|
||||
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"follower_connection_id" INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX
|
||||
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
|
||||
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
|
||||
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "worktree_entries"
|
||||
ADD COLUMN "scan_id" INT8,
|
||||
ADD COLUMN "is_deleted" BOOL;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE worktrees
|
||||
ALTER COLUMN is_complete SET DEFAULT FALSE,
|
||||
ADD COLUMN completed_scan_id INT8;
|
||||
15
crates/collab/migrations/20230202155735_followers.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS "followers" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"room_id" INTEGER NOT NULL REFERENCES rooms (id) ON DELETE CASCADE,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"leader_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"leader_connection_id" INTEGER NOT NULL,
|
||||
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"follower_connection_id" INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX
|
||||
"index_followers_on_project_id_and_leader_connection_server_id_and_leader_connection_id_and_follower_connection_server_id_and_follower_connection_id"
|
||||
ON "followers" ("project_id", "leader_connection_server_id", "leader_connection_id", "follower_connection_server_id", "follower_connection_id");
|
||||
|
||||
CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||
@@ -353,6 +353,8 @@ pub struct CreateInviteFromCodeParams {
|
||||
invite_code: String,
|
||||
email_address: String,
|
||||
device_id: Option<String>,
|
||||
#[serde(default)]
|
||||
added_to_mailing_list: bool,
|
||||
}
|
||||
|
||||
async fn create_invite_from_code(
|
||||
@@ -365,6 +367,7 @@ async fn create_invite_from_code(
|
||||
¶ms.invite_code,
|
||||
¶ms.email_address,
|
||||
params.device_id.as_deref(),
|
||||
params.added_to_mailing_list,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
|
||||
51
crates/collab/src/db/follower.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use super::{FollowerId, ProjectId, RoomId, ServerId};
|
||||
use rpc::ConnectionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
|
||||
#[sea_orm(table_name = "followers")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: FollowerId,
|
||||
pub room_id: RoomId,
|
||||
pub project_id: ProjectId,
|
||||
pub leader_connection_server_id: ServerId,
|
||||
pub leader_connection_id: i32,
|
||||
pub follower_connection_server_id: ServerId,
|
||||
pub follower_connection_id: i32,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn leader_connection(&self) -> ConnectionId {
|
||||
ConnectionId {
|
||||
owner_id: self.leader_connection_server_id.0 as u32,
|
||||
id: self.leader_connection_id as u32,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn follower_connection(&self) -> ConnectionId {
|
||||
ConnectionId {
|
||||
owner_id: self.follower_connection_server_id.0 as u32,
|
||||
id: self.follower_connection_id as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::room::Entity",
|
||||
from = "Column::RoomId",
|
||||
to = "super::room::Column::Id"
|
||||
)]
|
||||
Room,
|
||||
}
|
||||
|
||||
impl Related<super::room::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Room.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::{ProjectCollaboratorId, ProjectId, ReplicaId, ServerId, UserId};
|
||||
use rpc::ConnectionId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
@@ -14,6 +15,15 @@ pub struct Model {
|
||||
pub is_host: bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn connection(&self) -> ConnectionId {
|
||||
ConnectionId {
|
||||
owner_id: self.connection_server_id.0 as u32,
|
||||
id: self.connection_id as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
|
||||
@@ -15,6 +15,8 @@ pub enum Relation {
|
||||
RoomParticipant,
|
||||
#[sea_orm(has_many = "super::project::Entity")]
|
||||
Project,
|
||||
#[sea_orm(has_many = "super::follower::Entity")]
|
||||
Follower,
|
||||
}
|
||||
|
||||
impl Related<super::room_participant::Entity> for Entity {
|
||||
@@ -29,4 +31,10 @@ impl Related<super::project::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::follower::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Follower.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -567,7 +567,12 @@ async fn test_invite_codes() {
|
||||
|
||||
// User 2 redeems the invite code and becomes a contact of user 1.
|
||||
let user2_invite = db
|
||||
.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
|
||||
.create_invite_from_code(
|
||||
&invite_code,
|
||||
"user2@example.com",
|
||||
Some("user-2-device-id"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let NewUserResult {
|
||||
@@ -617,7 +622,7 @@ async fn test_invite_codes() {
|
||||
|
||||
// User 3 redeems the invite code and becomes a contact of user 1.
|
||||
let user3_invite = db
|
||||
.create_invite_from_code(&invite_code, "user3@example.com", None)
|
||||
.create_invite_from_code(&invite_code, "user3@example.com", None, true)
|
||||
.await
|
||||
.unwrap();
|
||||
let NewUserResult {
|
||||
@@ -672,9 +677,14 @@ async fn test_invite_codes() {
|
||||
);
|
||||
|
||||
// Trying to reedem the code for the third time results in an error.
|
||||
db.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
db.create_invite_from_code(
|
||||
&invite_code,
|
||||
"user4@example.com",
|
||||
Some("user-4-device-id"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Invite count can be updated after the code has been created.
|
||||
db.set_invite_count_for_user(user1, 2).await.unwrap();
|
||||
@@ -684,7 +694,12 @@ async fn test_invite_codes() {
|
||||
|
||||
// User 4 can now redeem the invite code and becomes a contact of user 1.
|
||||
let user4_invite = db
|
||||
.create_invite_from_code(&invite_code, "user4@example.com", Some("user-4-device-id"))
|
||||
.create_invite_from_code(
|
||||
&invite_code,
|
||||
"user4@example.com",
|
||||
Some("user-4-device-id"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let user4 = db
|
||||
@@ -739,9 +754,14 @@ async fn test_invite_codes() {
|
||||
);
|
||||
|
||||
// An existing user cannot redeem invite codes.
|
||||
db.create_invite_from_code(&invite_code, "user2@example.com", Some("user-2-device-id"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
db.create_invite_from_code(
|
||||
&invite_code,
|
||||
"user2@example.com",
|
||||
Some("user-2-device-id"),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
let (_, invite_count) = db.get_invite_code_for_user(user1).await.unwrap().unwrap();
|
||||
assert_eq!(invite_count, 1);
|
||||
|
||||
@@ -763,7 +783,7 @@ async fn test_invite_codes() {
|
||||
db.set_invite_count_for_user(user5, 5).await.unwrap();
|
||||
let (user5_invite_code, _) = db.get_invite_code_for_user(user5).await.unwrap().unwrap();
|
||||
let user5_invite_to_user1 = db
|
||||
.create_invite_from_code(&user5_invite_code, "user1@different.com", None)
|
||||
.create_invite_from_code(&user5_invite_code, "user1@different.com", None, true)
|
||||
.await
|
||||
.unwrap();
|
||||
let user1_2 = db
|
||||
|
||||
@@ -11,8 +11,10 @@ pub struct Model {
|
||||
pub abs_path: String,
|
||||
pub root_name: String,
|
||||
pub visible: bool,
|
||||
/// The last scan for which we've observed entries. It may be in progress.
|
||||
pub scan_id: i64,
|
||||
pub is_complete: bool,
|
||||
/// The last scan that fully completed.
|
||||
pub completed_scan_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -17,6 +17,8 @@ pub struct Model {
|
||||
pub mtime_nanos: i32,
|
||||
pub is_symlink: bool,
|
||||
pub is_ignored: bool,
|
||||
pub is_deleted: bool,
|
||||
pub scan_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -33,4 +33,12 @@ impl Executor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_backtrace(&self) {
|
||||
match self {
|
||||
Executor::Production => {}
|
||||
#[cfg(test)]
|
||||
Executor::Deterministic(background) => background.record_backtrace(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ pub mod auth;
|
||||
pub mod db;
|
||||
pub mod env;
|
||||
pub mod executor;
|
||||
#[cfg(test)]
|
||||
mod integration_tests;
|
||||
pub mod rpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use db::Database;
|
||||
use serde::Deserialize;
|
||||
@@ -90,6 +91,7 @@ impl std::error::Error for Error {}
|
||||
pub struct Config {
|
||||
pub http_port: u16,
|
||||
pub database_url: String,
|
||||
pub database_max_connections: u32,
|
||||
pub api_token: String,
|
||||
pub invite_link_prefix: String,
|
||||
pub live_kit_server: Option<String>,
|
||||
@@ -115,7 +117,7 @@ pub struct AppState {
|
||||
impl AppState {
|
||||
pub async fn new(config: Config) -> Result<Arc<Self>> {
|
||||
let mut db_options = db::ConnectOptions::new(config.database_url.clone());
|
||||
db_options.max_connections(5);
|
||||
db_options.max_connections(config.database_max_connections);
|
||||
let db = Database::new(db_options).await?;
|
||||
let live_kit_client = if let Some(((server, key), secret)) = config
|
||||
.live_kit_server
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{routing::get, Router};
|
||||
use axum::{routing::get, Extension, Router};
|
||||
use collab::{db, env, executor::Executor, AppState, Config, MigrateConfig, Result};
|
||||
use db::Database;
|
||||
use std::{
|
||||
env::args,
|
||||
net::{SocketAddr, TcpListener},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::signal::unix::SignalKind;
|
||||
use tracing_log::LogTracer;
|
||||
@@ -66,7 +67,12 @@ async fn main() -> Result<()> {
|
||||
|
||||
let app = collab::api::routes(rpc_server.clone(), state.clone())
|
||||
.merge(collab::rpc::routes(rpc_server.clone()))
|
||||
.merge(Router::new().route("/", get(handle_root)));
|
||||
.merge(
|
||||
Router::new()
|
||||
.route("/", get(handle_root))
|
||||
.route("/healthz", get(handle_liveness_probe))
|
||||
.layer(Extension(state.clone())),
|
||||
);
|
||||
|
||||
axum::Server::from_tcp(listener)?
|
||||
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
|
||||
@@ -95,6 +101,11 @@ async fn handle_root() -> String {
|
||||
format!("collab v{VERSION}")
|
||||
}
|
||||
|
||||
async fn handle_liveness_probe(Extension(state): Extension<Arc<AppState>>) -> Result<String> {
|
||||
state.db.get_all_users(0, 1).await?;
|
||||
Ok("ok".to_string())
|
||||
}
|
||||
|
||||
pub fn init_tracing(config: &Config) -> Option<()> {
|
||||
use std::str::FromStr;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
|
||||
@@ -53,11 +53,11 @@ use std::{
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::watch;
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::{info_span, instrument, Instrument};
|
||||
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
lazy_static! {
|
||||
@@ -95,6 +95,7 @@ struct Session {
|
||||
peer: Arc<Peer>,
|
||||
connection_pool: Arc<parking_lot::Mutex<ConnectionPool>>,
|
||||
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
executor: Executor,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -184,7 +185,8 @@ impl Server {
|
||||
.add_request_handler(ping)
|
||||
.add_request_handler(create_room)
|
||||
.add_request_handler(join_room)
|
||||
.add_message_handler(leave_room)
|
||||
.add_request_handler(rejoin_room)
|
||||
.add_request_handler(leave_room)
|
||||
.add_request_handler(call)
|
||||
.add_request_handler(cancel_call)
|
||||
.add_message_handler(decline_call)
|
||||
@@ -215,6 +217,7 @@ impl Server {
|
||||
.add_request_handler(forward_project_request::<proto::PrepareRename>)
|
||||
.add_request_handler(forward_project_request::<proto::PerformRename>)
|
||||
.add_request_handler(forward_project_request::<proto::ReloadBuffers>)
|
||||
.add_request_handler(forward_project_request::<proto::SynchronizeBuffers>)
|
||||
.add_request_handler(forward_project_request::<proto::FormatBuffers>)
|
||||
.add_request_handler(forward_project_request::<proto::CreateProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
|
||||
@@ -249,16 +252,6 @@ impl Server {
|
||||
let live_kit_client = self.app_state.live_kit_client.clone();
|
||||
|
||||
let span = info_span!("start server");
|
||||
let span_enter = span.enter();
|
||||
|
||||
tracing::info!("begin deleting stale projects");
|
||||
app_state
|
||||
.db
|
||||
.delete_stale_projects(&app_state.config.zed_environment, server_id)
|
||||
.await?;
|
||||
tracing::info!("finish deleting stale projects");
|
||||
|
||||
drop(span_enter);
|
||||
self.executor.spawn_detached(
|
||||
async move {
|
||||
tracing::info!("waiting for cleanup timeout");
|
||||
@@ -277,8 +270,11 @@ impl Server {
|
||||
let mut live_kit_room = String::new();
|
||||
let mut delete_live_kit_room = false;
|
||||
|
||||
if let Ok(mut refreshed_room) =
|
||||
app_state.db.refresh_room(room_id, server_id).await
|
||||
if let Some(mut refreshed_room) = app_state
|
||||
.db
|
||||
.refresh_room(room_id, server_id)
|
||||
.await
|
||||
.trace_err()
|
||||
{
|
||||
tracing::info!(
|
||||
room_id = room_id.0,
|
||||
@@ -354,7 +350,7 @@ impl Server {
|
||||
|
||||
app_state
|
||||
.db
|
||||
.delete_stale_servers(server_id, &app_state.config.zed_environment)
|
||||
.delete_stale_servers(&app_state.config.zed_environment, server_id)
|
||||
.await
|
||||
.trace_err();
|
||||
}
|
||||
@@ -529,7 +525,8 @@ impl Server {
|
||||
db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))),
|
||||
peer: this.peer.clone(),
|
||||
connection_pool: this.connection_pool.clone(),
|
||||
live_kit_client: this.app_state.live_kit_client.clone()
|
||||
live_kit_client: this.app_state.live_kit_client.clone(),
|
||||
executor: executor.clone(),
|
||||
};
|
||||
update_user_contacts(user_id, &session).await?;
|
||||
|
||||
@@ -545,8 +542,13 @@ impl Server {
|
||||
// This arrangement ensures we will attempt to process earlier messages first, but fall
|
||||
// back to processing messages arrived later in the spirit of making progress.
|
||||
let mut foreground_message_handlers = FuturesUnordered::new();
|
||||
let concurrent_handlers = Arc::new(Semaphore::new(256));
|
||||
loop {
|
||||
let next_message = incoming_rx.next().fuse();
|
||||
let next_message = async {
|
||||
let permit = concurrent_handlers.clone().acquire_owned().await.unwrap();
|
||||
let message = incoming_rx.next().await;
|
||||
(permit, message)
|
||||
}.fuse();
|
||||
futures::pin_mut!(next_message);
|
||||
futures::select_biased! {
|
||||
_ = teardown.changed().fuse() => return Ok(()),
|
||||
@@ -557,7 +559,8 @@ impl Server {
|
||||
break;
|
||||
}
|
||||
_ = foreground_message_handlers.next() => {}
|
||||
message = next_message => {
|
||||
next_message = next_message => {
|
||||
let (permit, message) = next_message;
|
||||
if let Some(message) = message {
|
||||
let type_name = message.payload_type_name();
|
||||
let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
|
||||
@@ -567,7 +570,10 @@ impl Server {
|
||||
let handle_message = (handler)(message, session.clone());
|
||||
drop(span_enter);
|
||||
|
||||
let handle_message = handle_message.instrument(span);
|
||||
let handle_message = async move {
|
||||
handle_message.await;
|
||||
drop(permit);
|
||||
}.instrument(span);
|
||||
if is_background {
|
||||
executor.spawn_detached(handle_message);
|
||||
} else {
|
||||
@@ -586,7 +592,7 @@ impl Server {
|
||||
|
||||
drop(foreground_message_handlers);
|
||||
tracing::info!(%user_id, %login, %connection_id, %address, "signing out");
|
||||
if let Err(error) = sign_out(session, teardown, executor).await {
|
||||
if let Err(error) = connection_lost(session, teardown, executor).await {
|
||||
tracing::error!(%user_id, %login, %connection_id, %address, ?error, "error signing out");
|
||||
}
|
||||
|
||||
@@ -678,15 +684,17 @@ impl<'a> Drop for ConnectionPoolGuard<'a> {
|
||||
}
|
||||
|
||||
fn broadcast<F>(
|
||||
sender_id: ConnectionId,
|
||||
sender_id: Option<ConnectionId>,
|
||||
receiver_ids: impl IntoIterator<Item = ConnectionId>,
|
||||
mut f: F,
|
||||
) where
|
||||
F: FnMut(ConnectionId) -> anyhow::Result<()>,
|
||||
{
|
||||
for receiver_id in receiver_ids {
|
||||
if receiver_id != sender_id {
|
||||
f(receiver_id).trace_err();
|
||||
if Some(receiver_id) != sender_id {
|
||||
if let Err(error) = f(receiver_id) {
|
||||
tracing::error!("failed to send to {:?} {}", receiver_id, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,7 +795,7 @@ pub async fn handle_metrics(Extension(server): Extension<Arc<Server>>) -> Result
|
||||
}
|
||||
|
||||
#[instrument(err, skip(executor))]
|
||||
async fn sign_out(
|
||||
async fn connection_lost(
|
||||
session: Session,
|
||||
mut teardown: watch::Receiver<()>,
|
||||
executor: Executor,
|
||||
@@ -798,17 +806,12 @@ async fn sign_out(
|
||||
.await
|
||||
.remove_connection(session.connection_id)?;
|
||||
|
||||
if let Some(mut left_projects) = session
|
||||
session
|
||||
.db()
|
||||
.await
|
||||
.connection_lost(session.connection_id)
|
||||
.await
|
||||
.trace_err()
|
||||
{
|
||||
for left_project in mem::take(&mut *left_projects) {
|
||||
project_left(&left_project, &session);
|
||||
}
|
||||
}
|
||||
.trace_err();
|
||||
|
||||
futures::select_biased! {
|
||||
_ = executor.sleep(RECONNECT_TIMEOUT).fuse() => {
|
||||
@@ -941,8 +944,172 @@ async fn join_room(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave_room(_message: proto::LeaveRoom, session: Session) -> Result<()> {
|
||||
leave_room_for_session(&session).await
|
||||
async fn rejoin_room(
|
||||
request: proto::RejoinRoom,
|
||||
response: Response<proto::RejoinRoom>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
{
|
||||
let mut rejoined_room = session
|
||||
.db()
|
||||
.await
|
||||
.rejoin_room(request, session.user_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
response.send(proto::RejoinRoomResponse {
|
||||
room: Some(rejoined_room.room.clone()),
|
||||
reshared_projects: rejoined_room
|
||||
.reshared_projects
|
||||
.iter()
|
||||
.map(|project| proto::ResharedProject {
|
||||
id: project.id.to_proto(),
|
||||
collaborators: project
|
||||
.collaborators
|
||||
.iter()
|
||||
.map(|collaborator| collaborator.to_proto())
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
rejoined_projects: rejoined_room
|
||||
.rejoined_projects
|
||||
.iter()
|
||||
.map(|rejoined_project| proto::RejoinedProject {
|
||||
id: rejoined_project.id.to_proto(),
|
||||
worktrees: rejoined_project
|
||||
.worktrees
|
||||
.iter()
|
||||
.map(|worktree| proto::WorktreeMetadata {
|
||||
id: worktree.id,
|
||||
root_name: worktree.root_name.clone(),
|
||||
visible: worktree.visible,
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
})
|
||||
.collect(),
|
||||
collaborators: rejoined_project
|
||||
.collaborators
|
||||
.iter()
|
||||
.map(|collaborator| collaborator.to_proto())
|
||||
.collect(),
|
||||
language_servers: rejoined_project.language_servers.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})?;
|
||||
room_updated(&rejoined_room.room, &session.peer);
|
||||
|
||||
for project in &rejoined_room.reshared_projects {
|
||||
for collaborator in &project.collaborators {
|
||||
session
|
||||
.peer
|
||||
.send(
|
||||
collaborator.connection_id,
|
||||
proto::UpdateProjectCollaborator {
|
||||
project_id: project.id.to_proto(),
|
||||
old_peer_id: Some(project.old_connection_id.into()),
|
||||
new_peer_id: Some(session.connection_id.into()),
|
||||
},
|
||||
)
|
||||
.trace_err();
|
||||
}
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
project
|
||||
.collaborators
|
||||
.iter()
|
||||
.map(|collaborator| collaborator.connection_id),
|
||||
|connection_id| {
|
||||
session.peer.forward_send(
|
||||
session.connection_id,
|
||||
connection_id,
|
||||
proto::UpdateProject {
|
||||
project_id: project.id.to_proto(),
|
||||
worktrees: project.worktrees.clone(),
|
||||
},
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for project in &rejoined_room.rejoined_projects {
|
||||
for collaborator in &project.collaborators {
|
||||
session
|
||||
.peer
|
||||
.send(
|
||||
collaborator.connection_id,
|
||||
proto::UpdateProjectCollaborator {
|
||||
project_id: project.id.to_proto(),
|
||||
old_peer_id: Some(project.old_connection_id.into()),
|
||||
new_peer_id: Some(session.connection_id.into()),
|
||||
},
|
||||
)
|
||||
.trace_err();
|
||||
}
|
||||
}
|
||||
|
||||
for project in &mut rejoined_room.rejoined_projects {
|
||||
for worktree in mem::take(&mut project.worktrees) {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
const MAX_CHUNK_SIZE: usize = 2;
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
const MAX_CHUNK_SIZE: usize = 256;
|
||||
|
||||
// Stream this worktree's entries.
|
||||
let message = proto::UpdateWorktree {
|
||||
project_id: project.id.to_proto(),
|
||||
worktree_id: worktree.id,
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
root_name: worktree.root_name,
|
||||
updated_entries: worktree.updated_entries,
|
||||
removed_entries: worktree.removed_entries,
|
||||
scan_id: worktree.scan_id,
|
||||
is_last_update: worktree.completed_scan_id == worktree.scan_id,
|
||||
};
|
||||
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||
session.peer.send(session.connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
// Stream this worktree's diagnostics.
|
||||
for summary in worktree.diagnostic_summaries {
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
proto::UpdateDiagnosticSummary {
|
||||
project_id: project.id.to_proto(),
|
||||
worktree_id: worktree.id,
|
||||
summary: Some(summary),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
for language_server in &project.language_servers {
|
||||
session.peer.send(
|
||||
session.connection_id,
|
||||
proto::UpdateLanguageServer {
|
||||
project_id: project.id.to_proto(),
|
||||
language_server_id: language_server.id,
|
||||
variant: Some(
|
||||
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
|
||||
proto::LspDiskBasedDiagnosticsUpdated {},
|
||||
),
|
||||
),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_user_contacts(session.user_id, &session).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave_room(
|
||||
_: proto::LeaveRoom,
|
||||
response: Response<proto::LeaveRoom>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
leave_room_for_session(&session).await?;
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn call(
|
||||
@@ -1132,7 +1299,7 @@ async fn unshare_project(message: proto::UnshareProject, session: Session) -> Re
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|conn_id| session.peer.send(conn_id, message.clone()),
|
||||
);
|
||||
@@ -1160,19 +1327,10 @@ async fn join_project(
|
||||
let collaborators = project
|
||||
.collaborators
|
||||
.iter()
|
||||
.map(|collaborator| {
|
||||
let peer_id = proto::PeerId {
|
||||
owner_id: collaborator.connection_server_id.0 as u32,
|
||||
id: collaborator.connection_id as u32,
|
||||
};
|
||||
proto::Collaborator {
|
||||
peer_id: Some(peer_id),
|
||||
replica_id: collaborator.replica_id.0 as u32,
|
||||
user_id: collaborator.user_id.to_proto(),
|
||||
}
|
||||
})
|
||||
.filter(|collaborator| collaborator.peer_id != Some(session.connection_id.into()))
|
||||
.filter(|collaborator| collaborator.connection_id != session.connection_id)
|
||||
.map(|collaborator| collaborator.to_proto())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let worktrees = project
|
||||
.worktrees
|
||||
.iter()
|
||||
@@ -1224,7 +1382,7 @@ async fn join_project(
|
||||
updated_entries: worktree.entries,
|
||||
removed_entries: Default::default(),
|
||||
scan_id: worktree.scan_id,
|
||||
is_last_update: worktree.is_complete,
|
||||
is_last_update: worktree.scan_id == worktree.completed_scan_id,
|
||||
};
|
||||
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||
session.peer.send(session.connection_id, update.clone())?;
|
||||
@@ -1265,7 +1423,7 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
||||
let sender_id = session.connection_id;
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
|
||||
let project = session
|
||||
let (room, project) = &*session
|
||||
.db()
|
||||
.await
|
||||
.leave_project(project_id, sender_id)
|
||||
@@ -1276,7 +1434,9 @@ async fn leave_project(request: proto::LeaveProject, session: Session) -> Result
|
||||
host_connection_id = %project.host_connection_id,
|
||||
"leave project"
|
||||
);
|
||||
|
||||
project_left(&project, &session);
|
||||
room_updated(&room, &session.peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1293,7 +1453,7 @@ async fn update_project(
|
||||
.update_project(project_id, session.connection_id, &request.worktrees)
|
||||
.await?;
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1319,7 +1479,7 @@ async fn update_worktree(
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1342,7 +1502,7 @@ async fn update_diagnostic_summary(
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1365,7 +1525,7 @@ async fn start_language_server(
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1380,6 +1540,7 @@ async fn update_language_server(
|
||||
request: proto::UpdateLanguageServer,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
session.executor.record_backtrace();
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
@@ -1387,7 +1548,7 @@ async fn update_language_server(
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1406,6 +1567,7 @@ async fn forward_project_request<T>(
|
||||
where
|
||||
T: EntityMessage + RequestMessage,
|
||||
{
|
||||
session.executor.record_backtrace();
|
||||
let project_id = ProjectId::from_proto(request.remote_entity_id());
|
||||
let host_connection_id = {
|
||||
let collaborators = session
|
||||
@@ -1413,14 +1575,11 @@ where
|
||||
.await
|
||||
.project_collaborators(project_id, session.connection_id)
|
||||
.await?;
|
||||
let host = collaborators
|
||||
collaborators
|
||||
.iter()
|
||||
.find(|collaborator| collaborator.is_host)
|
||||
.ok_or_else(|| anyhow!("host not found"))?;
|
||||
ConnectionId {
|
||||
owner_id: host.connection_server_id.0 as u32,
|
||||
id: host.connection_id as u32,
|
||||
}
|
||||
.ok_or_else(|| anyhow!("host not found"))?
|
||||
.connection_id
|
||||
};
|
||||
|
||||
let payload = session
|
||||
@@ -1444,14 +1603,11 @@ async fn save_buffer(
|
||||
.await
|
||||
.project_collaborators(project_id, session.connection_id)
|
||||
.await?;
|
||||
let host = collaborators
|
||||
collaborators
|
||||
.iter()
|
||||
.find(|collaborator| collaborator.is_host)
|
||||
.ok_or_else(|| anyhow!("host not found"))?;
|
||||
ConnectionId {
|
||||
owner_id: host.connection_server_id.0 as u32,
|
||||
id: host.connection_id as u32,
|
||||
}
|
||||
.ok_or_else(|| anyhow!("host not found"))?
|
||||
.connection_id
|
||||
};
|
||||
let response_payload = session
|
||||
.peer
|
||||
@@ -1463,22 +1619,19 @@ async fn save_buffer(
|
||||
.await
|
||||
.project_collaborators(project_id, session.connection_id)
|
||||
.await?;
|
||||
collaborators.retain(|collaborator| {
|
||||
let collaborator_connection = ConnectionId {
|
||||
owner_id: collaborator.connection_server_id.0 as u32,
|
||||
id: collaborator.connection_id as u32,
|
||||
};
|
||||
collaborator_connection != session.connection_id
|
||||
});
|
||||
let project_connection_ids = collaborators.iter().map(|collaborator| ConnectionId {
|
||||
owner_id: collaborator.connection_server_id.0 as u32,
|
||||
id: collaborator.connection_id as u32,
|
||||
});
|
||||
broadcast(host_connection_id, project_connection_ids, |conn_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(host_connection_id, conn_id, response_payload.clone())
|
||||
});
|
||||
collaborators.retain(|collaborator| collaborator.connection_id != session.connection_id);
|
||||
let project_connection_ids = collaborators
|
||||
.iter()
|
||||
.map(|collaborator| collaborator.connection_id);
|
||||
broadcast(
|
||||
Some(host_connection_id),
|
||||
project_connection_ids,
|
||||
|conn_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(host_connection_id, conn_id, response_payload.clone())
|
||||
},
|
||||
);
|
||||
response.send(response_payload)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1487,6 +1640,7 @@ async fn create_buffer_for_peer(
|
||||
request: proto::CreateBufferForPeer,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
session.executor.record_backtrace();
|
||||
let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
|
||||
session
|
||||
.peer
|
||||
@@ -1499,6 +1653,7 @@ async fn update_buffer(
|
||||
response: Response<proto::UpdateBuffer>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
session.executor.record_backtrace();
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
@@ -1506,8 +1661,10 @@ async fn update_buffer(
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
|
||||
session.executor.record_backtrace();
|
||||
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1528,7 +1685,7 @@ async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session)
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1547,7 +1704,7 @@ async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Re
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1566,7 +1723,7 @@ async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<(
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1588,6 +1745,7 @@ async fn follow(
|
||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||
.into();
|
||||
let follower_id = session.connection_id;
|
||||
|
||||
{
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
@@ -1608,6 +1766,14 @@ async fn follow(
|
||||
.views
|
||||
.retain(|view| view.leader_id != Some(follower_id.into()));
|
||||
response.send(response_payload)?;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.follow(project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1617,17 +1783,29 @@ async fn unfollow(request: proto::Unfollow, session: Session) -> Result<()> {
|
||||
.leader_id
|
||||
.ok_or_else(|| anyhow!("invalid leader id"))?
|
||||
.into();
|
||||
let project_connection_ids = session
|
||||
let follower_id = session.connection_id;
|
||||
|
||||
if !session
|
||||
.db()
|
||||
.await
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
if !project_connection_ids.contains(&leader_id) {
|
||||
.await?
|
||||
.contains(&leader_id)
|
||||
{
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, leader_id, request)?;
|
||||
|
||||
let room = session
|
||||
.db()
|
||||
.await
|
||||
.unfollow(project_id, leader_id, follower_id)
|
||||
.await?;
|
||||
room_updated(&room, &session.peer);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1825,23 +2003,31 @@ async fn remove_contact(
|
||||
let requester_id = session.user_id;
|
||||
let responder_id = UserId::from_proto(request.user_id);
|
||||
let db = session.db().await;
|
||||
db.remove_contact(requester_id, responder_id).await?;
|
||||
let contact_accepted = db.remove_contact(requester_id, responder_id).await?;
|
||||
|
||||
let pool = session.connection_pool().await;
|
||||
// Update outgoing contact requests of requester
|
||||
let mut update = proto::UpdateContacts::default();
|
||||
update
|
||||
.remove_outgoing_requests
|
||||
.push(responder_id.to_proto());
|
||||
if contact_accepted {
|
||||
update.remove_contacts.push(responder_id.to_proto());
|
||||
} else {
|
||||
update
|
||||
.remove_outgoing_requests
|
||||
.push(responder_id.to_proto());
|
||||
}
|
||||
for connection_id in pool.user_connection_ids(requester_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
// Update incoming contact requests of responder
|
||||
let mut update = proto::UpdateContacts::default();
|
||||
update
|
||||
.remove_incoming_requests
|
||||
.push(requester_id.to_proto());
|
||||
if contact_accepted {
|
||||
update.remove_contacts.push(requester_id.to_proto());
|
||||
} else {
|
||||
update
|
||||
.remove_incoming_requests
|
||||
.push(requester_id.to_proto());
|
||||
}
|
||||
for connection_id in pool.user_connection_ids(responder_id) {
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
@@ -1858,7 +2044,7 @@ async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> R
|
||||
.project_connection_ids(project_id, session.connection_id)
|
||||
.await?;
|
||||
broadcast(
|
||||
session.connection_id,
|
||||
Some(session.connection_id),
|
||||
project_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
@@ -1968,21 +2154,20 @@ fn contact_for_user(
|
||||
}
|
||||
|
||||
fn room_updated(room: &proto::Room, peer: &Peer) {
|
||||
for participant in &room.participants {
|
||||
if let Some(peer_id) = participant
|
||||
.peer_id
|
||||
.ok_or_else(|| anyhow!("invalid participant peer id"))
|
||||
.trace_err()
|
||||
{
|
||||
broadcast(
|
||||
None,
|
||||
room.participants
|
||||
.iter()
|
||||
.filter_map(|participant| Some(participant.peer_id?.into())),
|
||||
|peer_id| {
|
||||
peer.send(
|
||||
peer_id.into(),
|
||||
proto::RoomUpdated {
|
||||
room: Some(room.clone()),
|
||||
},
|
||||
)
|
||||
.trace_err();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async fn update_user_contacts(user_id: UserId, session: &Session) -> Result<()> {
|
||||
@@ -2103,16 +2288,6 @@ fn project_left(project: &db::LeftProject, session: &Session) {
|
||||
.trace_err();
|
||||
}
|
||||
}
|
||||
|
||||
session
|
||||
.peer
|
||||
.send(
|
||||
session.connection_id,
|
||||
proto::UnshareProject {
|
||||
project_id: project.id.to_proto(),
|
||||
},
|
||||
)
|
||||
.trace_err();
|
||||
}
|
||||
|
||||
pub trait ResultExt {
|
||||
|
||||
455
crates/collab/src/tests.rs
Normal file
@@ -0,0 +1,455 @@
|
||||
use crate::{
|
||||
db::{NewUserParams, TestDb, UserId},
|
||||
executor::Executor,
|
||||
rpc::{Server, CLEANUP_TIMEOUT},
|
||||
AppState,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use call::ActiveCall;
|
||||
use client::{
|
||||
self, proto::PeerId, test::FakeHttpClient, Client, Connection, Credentials,
|
||||
EstablishConnectionError, UserStore,
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{
|
||||
executor::Deterministic, test::EmptyView, ModelHandle, Task, TestAppContext, ViewHandle,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
env,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use theme::ThemeRegistry;
|
||||
use workspace::Workspace;
|
||||
|
||||
mod integration_tests;
|
||||
mod randomized_integration_tests;
|
||||
|
||||
struct TestServer {
|
||||
app_state: Arc<AppState>,
|
||||
server: Arc<Server>,
|
||||
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
|
||||
forbid_connections: Arc<AtomicBool>,
|
||||
_test_db: TestDb,
|
||||
test_live_kit_server: Arc<live_kit_client::TestServer>,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
async fn start(deterministic: &Arc<Deterministic>) -> Self {
|
||||
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
let use_postgres = env::var("USE_POSTGRES").ok();
|
||||
let use_postgres = use_postgres.as_deref();
|
||||
let test_db = if use_postgres == Some("true") || use_postgres == Some("1") {
|
||||
TestDb::postgres(deterministic.build_background())
|
||||
} else {
|
||||
TestDb::sqlite(deterministic.build_background())
|
||||
};
|
||||
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
|
||||
let live_kit_server = live_kit_client::TestServer::create(
|
||||
format!("http://livekit.{}.test", live_kit_server_id),
|
||||
format!("devkey-{}", live_kit_server_id),
|
||||
format!("secret-{}", live_kit_server_id),
|
||||
deterministic.build_background(),
|
||||
)
|
||||
.unwrap();
|
||||
let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
|
||||
let epoch = app_state
|
||||
.db
|
||||
.create_server(&app_state.config.zed_environment)
|
||||
.await
|
||||
.unwrap();
|
||||
let server = Server::new(
|
||||
epoch,
|
||||
app_state.clone(),
|
||||
Executor::Deterministic(deterministic.build_background()),
|
||||
);
|
||||
server.start().await.unwrap();
|
||||
// Advance clock to ensure the server's cleanup task is finished.
|
||||
deterministic.advance_clock(CLEANUP_TIMEOUT);
|
||||
Self {
|
||||
app_state,
|
||||
server,
|
||||
connection_killers: Default::default(),
|
||||
forbid_connections: Default::default(),
|
||||
_test_db: test_db,
|
||||
test_live_kit_server: live_kit_server,
|
||||
}
|
||||
}
|
||||
|
||||
async fn reset(&self) {
|
||||
self.app_state.db.reset();
|
||||
let epoch = self
|
||||
.app_state
|
||||
.db
|
||||
.create_server(&self.app_state.config.zed_environment)
|
||||
.await
|
||||
.unwrap();
|
||||
self.server.reset(epoch);
|
||||
}
|
||||
|
||||
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(Settings::test(cx));
|
||||
});
|
||||
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let user_id = if let Ok(Some(user)) = self
|
||||
.app_state
|
||||
.db
|
||||
.get_user_by_github_account(name, None)
|
||||
.await
|
||||
{
|
||||
user.id
|
||||
} else {
|
||||
self.app_state
|
||||
.db
|
||||
.create_user(
|
||||
&format!("{name}@example.com"),
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: name.into(),
|
||||
github_user_id: 0,
|
||||
invite_count: 0,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("creating user failed")
|
||||
.user_id
|
||||
};
|
||||
let client_name = name.to_string();
|
||||
let mut client = cx.read(|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();
|
||||
let forbid_connections = self.forbid_connections.clone();
|
||||
|
||||
Arc::get_mut(&mut client)
|
||||
.unwrap()
|
||||
.set_id(user_id.0 as usize)
|
||||
.override_authenticate(move |cx| {
|
||||
cx.spawn(|_| async move {
|
||||
let access_token = "the-token".to_string();
|
||||
Ok(Credentials {
|
||||
user_id: user_id.0 as u64,
|
||||
access_token,
|
||||
})
|
||||
})
|
||||
})
|
||||
.override_establish_connection(move |credentials, cx| {
|
||||
assert_eq!(credentials.user_id, user_id.0 as u64);
|
||||
assert_eq!(credentials.access_token, "the-token");
|
||||
|
||||
let server = server.clone();
|
||||
let db = db.clone();
|
||||
let connection_killers = connection_killers.clone();
|
||||
let forbid_connections = forbid_connections.clone();
|
||||
let client_name = client_name.clone();
|
||||
cx.spawn(move |cx| async move {
|
||||
if forbid_connections.load(SeqCst) {
|
||||
Err(EstablishConnectionError::other(anyhow!(
|
||||
"server is forbidding connections"
|
||||
)))
|
||||
} else {
|
||||
let (client_conn, server_conn, killed) =
|
||||
Connection::in_memory(cx.background());
|
||||
let (connection_id_tx, connection_id_rx) = oneshot::channel();
|
||||
let user = db
|
||||
.get_user_by_id(user_id)
|
||||
.await
|
||||
.expect("retrieving user failed")
|
||||
.unwrap();
|
||||
cx.background()
|
||||
.spawn(server.handle_connection(
|
||||
server_conn,
|
||||
client_name,
|
||||
user,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background()),
|
||||
))
|
||||
.detach();
|
||||
let connection_id = connection_id_rx.await.unwrap();
|
||||
connection_killers
|
||||
.lock()
|
||||
.insert(connection_id.into(), killed);
|
||||
Ok(client_conn)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
|
||||
let app_state = Arc::new(workspace::AppState {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
|
||||
themes: ThemeRegistry::new((), cx.font_cache()),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| None,
|
||||
background_actions: || &[],
|
||||
});
|
||||
|
||||
Project::init(&client);
|
||||
cx.update(|cx| {
|
||||
workspace::init(app_state.clone(), cx);
|
||||
call::init(client.clone(), user_store.clone(), cx);
|
||||
});
|
||||
|
||||
client
|
||||
.authenticate_and_connect(false, &cx.to_async())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let client = TestClient {
|
||||
client,
|
||||
username: name.to_string(),
|
||||
local_projects: Default::default(),
|
||||
remote_projects: Default::default(),
|
||||
next_root_dir_id: 0,
|
||||
user_store,
|
||||
fs,
|
||||
language_registry: Arc::new(LanguageRegistry::test()),
|
||||
buffers: Default::default(),
|
||||
};
|
||||
client.wait_for_current_user(cx).await;
|
||||
client
|
||||
}
|
||||
|
||||
fn disconnect_client(&self, peer_id: PeerId) {
|
||||
self.connection_killers
|
||||
.lock()
|
||||
.remove(&peer_id)
|
||||
.unwrap()
|
||||
.store(true, SeqCst);
|
||||
}
|
||||
|
||||
fn forbid_connections(&self) {
|
||||
self.forbid_connections.store(true, SeqCst);
|
||||
}
|
||||
|
||||
fn allow_connections(&self) {
|
||||
self.forbid_connections.store(false, SeqCst);
|
||||
}
|
||||
|
||||
async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
|
||||
for ix in 1..clients.len() {
|
||||
let (left, right) = clients.split_at_mut(ix);
|
||||
let (client_a, cx_a) = left.last_mut().unwrap();
|
||||
for (client_b, cx_b) in right {
|
||||
client_a
|
||||
.user_store
|
||||
.update(*cx_a, |store, cx| {
|
||||
store.request_contact(client_b.user_id().unwrap(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().run_until_parked();
|
||||
client_b
|
||||
.user_store
|
||||
.update(*cx_b, |store, cx| {
|
||||
store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
|
||||
self.make_contacts(clients).await;
|
||||
|
||||
let (left, right) = clients.split_at_mut(1);
|
||||
let (_client_a, cx_a) = &mut left[0];
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
for (client_b, cx_b) in right {
|
||||
let user_id_b = client_b.current_user_id(*cx_b).to_proto();
|
||||
active_call_a
|
||||
.update(*cx_a, |call, cx| call.invite(user_id_b, None, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_b.foreground().run_until_parked();
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
active_call_b
|
||||
.update(*cx_b, |call, cx| call.accept_incoming(cx))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_app_state(
|
||||
test_db: &TestDb,
|
||||
fake_server: &live_kit_client::TestServer,
|
||||
) -> Arc<AppState> {
|
||||
Arc::new(AppState {
|
||||
db: test_db.db().clone(),
|
||||
live_kit_client: Some(Arc::new(fake_server.create_api_client())),
|
||||
config: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TestServer {
|
||||
type Target = Server;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.server
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestServer {
|
||||
fn drop(&mut self) {
|
||||
self.server.teardown();
|
||||
self.test_live_kit_server.teardown().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
struct TestClient {
|
||||
client: Arc<Client>,
|
||||
username: String,
|
||||
local_projects: Vec<ModelHandle<Project>>,
|
||||
remote_projects: Vec<ModelHandle<Project>>,
|
||||
next_root_dir_id: usize,
|
||||
pub user_store: ModelHandle<UserStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
fs: Arc<FakeFs>,
|
||||
buffers: HashMap<ModelHandle<Project>, HashSet<ModelHandle<language::Buffer>>>,
|
||||
}
|
||||
|
||||
impl Deref for TestClient {
|
||||
type Target = Arc<Client>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactsSummary {
|
||||
pub current: Vec<String>,
|
||||
pub outgoing_requests: Vec<String>,
|
||||
pub incoming_requests: Vec<String>,
|
||||
}
|
||||
|
||||
impl TestClient {
|
||||
pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
|
||||
UserId::from_proto(
|
||||
self.user_store
|
||||
.read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
|
||||
)
|
||||
}
|
||||
|
||||
async fn wait_for_current_user(&self, cx: &TestAppContext) {
|
||||
let mut authed_user = self
|
||||
.user_store
|
||||
.read_with(cx, |user_store, _| user_store.watch_current_user());
|
||||
while authed_user.next().await.unwrap().is_none() {}
|
||||
}
|
||||
|
||||
async fn clear_contacts(&self, cx: &mut TestAppContext) {
|
||||
self.user_store
|
||||
.update(cx, |store, _| store.clear_contacts())
|
||||
.await;
|
||||
}
|
||||
|
||||
fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
|
||||
self.user_store.read_with(cx, |store, _| ContactsSummary {
|
||||
current: store
|
||||
.contacts()
|
||||
.iter()
|
||||
.map(|contact| contact.user.github_login.clone())
|
||||
.collect(),
|
||||
outgoing_requests: store
|
||||
.outgoing_contact_requests()
|
||||
.iter()
|
||||
.map(|user| user.github_login.clone())
|
||||
.collect(),
|
||||
incoming_requests: store
|
||||
.incoming_contact_requests()
|
||||
.iter()
|
||||
.map(|user| user.github_login.clone())
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_local_project(
|
||||
&self,
|
||||
root_path: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (ModelHandle<Project>, WorktreeId) {
|
||||
let project = cx.update(|cx| {
|
||||
Project::local(
|
||||
self.client.clone(),
|
||||
self.user_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (worktree, _) = project
|
||||
.update(cx, |p, cx| {
|
||||
p.find_or_create_local_worktree(root_path, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
(project, worktree.read_with(cx, |tree, _| tree.id()))
|
||||
}
|
||||
|
||||
async fn build_remote_project(
|
||||
&self,
|
||||
host_project_id: u64,
|
||||
guest_cx: &mut TestAppContext,
|
||||
) -> ModelHandle<Project> {
|
||||
let active_call = guest_cx.read(ActiveCall::global);
|
||||
let room = active_call.read_with(guest_cx, |call, _| call.room().unwrap().clone());
|
||||
room.update(guest_cx, |room, cx| {
|
||||
room.join_project(
|
||||
host_project_id,
|
||||
self.language_registry.clone(),
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn build_workspace(
|
||||
&self,
|
||||
project: &ModelHandle<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> ViewHandle<Workspace> {
|
||||
let (_, root_view) = cx.add_window(|_| EmptyView);
|
||||
cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
|
||||
}
|
||||
|
||||
fn create_new_root_dir(&mut self) -> PathBuf {
|
||||
format!(
|
||||
"/{}-root-{}",
|
||||
self.username,
|
||||
util::post_inc(&mut self.next_root_dir_id)
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestClient {
|
||||
fn drop(&mut self) {
|
||||
self.client.teardown();
|
||||
}
|
||||
}
|
||||
1229
crates/collab/src/tests/randomized_integration_tests.rs
Normal file
@@ -2,6 +2,7 @@
|
||||
name = "collab_ui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/collab_ui.rs"
|
||||
@@ -21,11 +22,14 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
auto_update = { path = "../auto_update" }
|
||||
call = { path = "../call" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
editor = { path = "../editor" }
|
||||
feedback = { path = "../feedback" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
mod collab_titlebar_item;
|
||||
mod collaborator_list_popover;
|
||||
mod contact_finder;
|
||||
mod contact_list;
|
||||
mod contact_notification;
|
||||
mod contacts_popover;
|
||||
mod face_pile;
|
||||
mod incoming_call_notification;
|
||||
mod notifications;
|
||||
mod project_shared_notification;
|
||||
mod sharing_status_indicator;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use call::ActiveCall;
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleCollaborationMenu};
|
||||
use gpui::MutableAppContext;
|
||||
use project::Project;
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use gpui::{actions, MutableAppContext, Task};
|
||||
use std::sync::Arc;
|
||||
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
|
||||
|
||||
actions!(collab, [ToggleScreenSharing]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
collab_titlebar_item::init(cx);
|
||||
contact_notification::init(cx);
|
||||
@@ -22,81 +27,108 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
contacts_popover::init(cx);
|
||||
incoming_call_notification::init(cx);
|
||||
project_shared_notification::init(cx);
|
||||
sharing_status_indicator::init(cx);
|
||||
|
||||
cx.add_global_action(toggle_screen_sharing);
|
||||
cx.add_global_action(move |action: &JoinProject, cx| {
|
||||
let project_id = action.project_id;
|
||||
let follow_user_id = action.follow_user_id;
|
||||
let app_state = app_state.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let existing_workspace = cx.update(|cx| {
|
||||
cx.window_ids()
|
||||
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
|
||||
.find(|workspace| {
|
||||
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
|
||||
})
|
||||
});
|
||||
join_project(action, app_state.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
let workspace = if let Some(existing_workspace) = existing_workspace {
|
||||
existing_workspace
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut MutableAppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
let project = Project::remote(
|
||||
project_id,
|
||||
app_state.client.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
cx.clone(),
|
||||
)
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
let project_id = action.project_id;
|
||||
let follow_user_id = action.follow_user_id;
|
||||
cx.spawn(|mut cx| async move {
|
||||
let existing_workspace = cx.update(|cx| {
|
||||
cx.window_ids()
|
||||
.filter_map(|window_id| cx.root_view::<Workspace>(window_id))
|
||||
.find(|workspace| {
|
||||
workspace.read(cx).project().read(cx).remote_id() == Some(project_id)
|
||||
})
|
||||
});
|
||||
|
||||
let workspace = if let Some(existing_workspace) = existing_workspace {
|
||||
existing_workspace
|
||||
} else {
|
||||
let active_call = cx.read(ActiveCall::global);
|
||||
let room = active_call
|
||||
.read_with(&cx, |call, _| call.room().cloned())
|
||||
.ok_or_else(|| anyhow!("not in a call"))?;
|
||||
let project = room
|
||||
.update(&mut cx, |room, cx| {
|
||||
room.join_project(
|
||||
project_id,
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
|
||||
let (_, workspace) = cx.add_window(
|
||||
(app_state.build_window_options)(None, None, cx.platform().as_ref()),
|
||||
|cx| {
|
||||
let mut workspace = Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project,
|
||||
app_state.dock_default_item_factory,
|
||||
app_state.background_actions,
|
||||
cx,
|
||||
);
|
||||
(app_state.initialize_workspace)(&mut workspace, &app_state, cx);
|
||||
workspace
|
||||
});
|
||||
workspace
|
||||
};
|
||||
},
|
||||
);
|
||||
workspace
|
||||
};
|
||||
|
||||
cx.activate_window(workspace.window_id());
|
||||
cx.platform().activate(true);
|
||||
cx.activate_window(workspace.window_id());
|
||||
cx.platform().activate(true);
|
||||
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
let follow_peer_id = room
|
||||
.read(cx)
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.find(|(_, participant)| participant.user.id == follow_user_id)
|
||||
.map(|(_, p)| p.peer_id)
|
||||
.or_else(|| {
|
||||
// If we couldn't follow the given user, follow the host instead.
|
||||
let collaborator = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.collaborators()
|
||||
.values()
|
||||
.find(|collaborator| collaborator.replica_id == 0)?;
|
||||
Some(collaborator.peer_id)
|
||||
});
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
let follow_peer_id = room
|
||||
.read(cx)
|
||||
.remote_participants()
|
||||
.iter()
|
||||
.find(|(_, participant)| participant.user.id == follow_user_id)
|
||||
.map(|(_, p)| p.peer_id)
|
||||
.or_else(|| {
|
||||
// If we couldn't follow the given user, follow the host instead.
|
||||
let collaborator = workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.collaborators()
|
||||
.values()
|
||||
.find(|collaborator| collaborator.replica_id == 0)?;
|
||||
Some(collaborator.peer_id)
|
||||
});
|
||||
|
||||
if let Some(follow_peer_id) = follow_peer_id {
|
||||
if !workspace.is_following(follow_peer_id) {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
|
||||
.map(|follow| follow.detach_and_log_err(cx));
|
||||
}
|
||||
if let Some(follow_peer_id) = follow_peer_id {
|
||||
if !workspace.is_being_followed(follow_peer_id) {
|
||||
workspace
|
||||
.toggle_follow(&ToggleFollow(follow_peer_id), cx)
|
||||
.map(|follow| follow.detach_and_log_err(cx));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
165
crates/collab_ui/src/collaborator_list_popover.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use call::ActiveCall;
|
||||
use client::UserStore;
|
||||
use gpui::Action;
|
||||
use gpui::{
|
||||
actions, elements::*, Entity, ModelHandle, MouseButton, RenderContext, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::collab_titlebar_item::ToggleCollaboratorList;
|
||||
|
||||
pub(crate) enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
enum Collaborator {
|
||||
SelfUser { username: String },
|
||||
RemoteUser { username: String },
|
||||
}
|
||||
|
||||
actions!(collaborator_list_popover, [NoOp]);
|
||||
|
||||
pub(crate) struct CollaboratorListPopover {
|
||||
list_state: ListState,
|
||||
}
|
||||
|
||||
impl Entity for CollaboratorListPopover {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for CollaboratorListPopover {
|
||||
fn ui_name() -> &'static str {
|
||||
"CollaboratorListPopover"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
||||
List::new(self.list_state.clone())
|
||||
.contained()
|
||||
.with_style(theme.contacts_popover.container) //TODO: Change the name of this theme key
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
.boxed()
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleCollaboratorList);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
}
|
||||
|
||||
impl CollaboratorListPopover {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
let mut collaborators = user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.map(|u| Collaborator::SelfUser {
|
||||
username: u.github_login.clone(),
|
||||
})
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
//TODO: What should the canonical sort here look like, consult contacts list implementation
|
||||
if let Some(room) = active_call.read(cx).room() {
|
||||
for participant in room.read(cx).remote_participants() {
|
||||
collaborators.push(Collaborator::RemoteUser {
|
||||
username: participant.1.user.github_login.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
list_state: ListState::new(
|
||||
collaborators.len(),
|
||||
Orientation::Top,
|
||||
0.,
|
||||
cx,
|
||||
move |_, index, cx| match &collaborators[index] {
|
||||
Collaborator::SelfUser { username } => render_collaborator_list_entry(
|
||||
index,
|
||||
username,
|
||||
None::<NoOp>,
|
||||
None,
|
||||
Svg::new("icons/chevron_right_12.svg"),
|
||||
NoOp,
|
||||
"Leave call".to_owned(),
|
||||
cx,
|
||||
),
|
||||
|
||||
Collaborator::RemoteUser { username } => render_collaborator_list_entry(
|
||||
index,
|
||||
username,
|
||||
Some(NoOp),
|
||||
Some(format!("Follow {username}")),
|
||||
Svg::new("icons/x_mark_12.svg"),
|
||||
NoOp,
|
||||
format!("Remove {username} from call"),
|
||||
cx,
|
||||
),
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_collaborator_list_entry<UA: Action + Clone, IA: Action + Clone>(
|
||||
index: usize,
|
||||
username: &str,
|
||||
username_action: Option<UA>,
|
||||
username_tooltip: Option<String>,
|
||||
icon: Svg,
|
||||
icon_action: IA,
|
||||
icon_tooltip: String,
|
||||
cx: &mut RenderContext<CollaboratorListPopover>,
|
||||
) -> ElementBox {
|
||||
enum Username {}
|
||||
enum UsernameTooltip {}
|
||||
enum Icon {}
|
||||
enum IconTooltip {}
|
||||
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
let username_theme = theme.contact_list.contact_username.text.clone();
|
||||
let tooltip_theme = theme.tooltip.clone();
|
||||
|
||||
let username = MouseEventHandler::<Username>::new(index, cx, |_, _| {
|
||||
Label::new(username.to_owned(), username_theme.clone()).boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if let Some(username_action) = username_action.clone() {
|
||||
cx.dispatch_action(username_action);
|
||||
}
|
||||
});
|
||||
|
||||
Flex::row()
|
||||
.with_child(if let Some(username_tooltip) = username_tooltip {
|
||||
username
|
||||
.with_tooltip::<UsernameTooltip, _>(
|
||||
index,
|
||||
username_tooltip,
|
||||
None,
|
||||
tooltip_theme.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
username.boxed()
|
||||
})
|
||||
.with_child(
|
||||
MouseEventHandler::<Icon>::new(index, cx, |_, _| icon.boxed())
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(icon_action.clone())
|
||||
})
|
||||
.with_tooltip::<IconTooltip, _>(index, icon_tooltip, None, tooltip_theme, cx)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{
|
||||
elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext,
|
||||
Task, View, ViewContext, ViewHandle,
|
||||
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, MutableAppContext,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
@@ -68,7 +68,7 @@ impl PickerDelegate for ContactFinder {
|
||||
this.potential_contacts = potential_contacts.into();
|
||||
cx.notify();
|
||||
});
|
||||
Ok(())
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
@@ -128,7 +128,7 @@ impl PickerDelegate for ContactFinder {
|
||||
.style_for(mouse_state, selected);
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_finder.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -178,4 +178,14 @@ impl ContactFinder {
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor_text(&self, cx: &AppContext) -> String {
|
||||
self.picker.read(cx).query(cx)
|
||||
}
|
||||
|
||||
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
|
||||
self.picker
|
||||
.update(cx, |picker, cx| picker.set_query(editor_text, cx));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,38 @@
|
||||
use std::{mem, sync::Arc};
|
||||
|
||||
use super::collab_titlebar_item::LeaveCall;
|
||||
use crate::contacts_popover;
|
||||
use call::ActiveCall;
|
||||
use client::{proto::PeerId, Contact, User, UserStore};
|
||||
use editor::{Cancel, Editor};
|
||||
use futures::StreamExt;
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
impl_actions, impl_internal_actions, keymap, AppContext, CursorStyle, Entity, ModelHandle,
|
||||
MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
impl_actions, impl_internal_actions,
|
||||
keymap_matcher::KeymapContext,
|
||||
AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, PromptLevel,
|
||||
RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{mem, sync::Arc};
|
||||
use theme::IconButton;
|
||||
use util::ResultExt;
|
||||
use workspace::{JoinProject, OpenSharedScreen};
|
||||
|
||||
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
|
||||
impl_internal_actions!(contact_list, [ToggleExpanded, Call, LeaveCall]);
|
||||
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ContactList::remove_contact);
|
||||
cx.add_action(ContactList::respond_to_contact_request);
|
||||
cx.add_action(ContactList::clear_filter);
|
||||
cx.add_action(ContactList::cancel);
|
||||
cx.add_action(ContactList::select_next);
|
||||
cx.add_action(ContactList::select_prev);
|
||||
cx.add_action(ContactList::confirm);
|
||||
cx.add_action(ContactList::toggle_expanded);
|
||||
cx.add_action(ContactList::call);
|
||||
cx.add_action(ContactList::leave_call);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@@ -43,9 +44,6 @@ struct Call {
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
struct LeaveCall;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
||||
enum Section {
|
||||
ActiveCall,
|
||||
@@ -143,7 +141,10 @@ impl PartialEq for ContactEntry {
|
||||
pub struct RequestContact(pub u64);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct RemoveContact(pub u64);
|
||||
pub struct RemoveContact {
|
||||
user_id: u64,
|
||||
github_login: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
pub struct RespondToContactRequest {
|
||||
@@ -296,10 +297,34 @@ impl ContactList {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn editor_text(&self, cx: &AppContext) -> String {
|
||||
self.filter_editor.read(cx).text(cx)
|
||||
}
|
||||
|
||||
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
|
||||
self.filter_editor
|
||||
.update(cx, |picker, cx| picker.set_text(editor_text, cx));
|
||||
self
|
||||
}
|
||||
|
||||
fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext<Self>) {
|
||||
self.user_store
|
||||
.update(cx, |store, cx| store.remove_contact(request.0, cx))
|
||||
.detach();
|
||||
let user_id = request.user_id;
|
||||
let github_login = &request.github_login;
|
||||
let user_store = self.user_store.clone();
|
||||
let prompt_message = format!(
|
||||
"Are you sure you want to remove \"{}\" from your contacts?",
|
||||
github_login
|
||||
);
|
||||
let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]);
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
if answer.next().await == Some(0) {
|
||||
user_store
|
||||
.update(&mut cx, |store, cx| store.remove_contact(user_id, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn respond_to_contact_request(
|
||||
@@ -314,7 +339,7 @@ impl ContactList {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
let did_clear = self.filter_editor.update(cx, |editor, cx| {
|
||||
if editor.buffer().read(cx).len(cx) > 0 {
|
||||
editor.set_text("", cx);
|
||||
@@ -323,6 +348,7 @@ impl ContactList {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !did_clear {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
@@ -717,7 +743,7 @@ impl ContactList {
|
||||
) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -737,7 +763,7 @@ impl ContactList {
|
||||
)
|
||||
.with_children(if is_pending {
|
||||
Some(
|
||||
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
|
||||
Label::new("Calling", theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.calling_indicator.container)
|
||||
.aligned()
|
||||
@@ -938,7 +964,7 @@ impl ContactList {
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new("Screen".into(), row.name.text.clone())
|
||||
Label::new("Screen", row.name.text.clone())
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
@@ -968,6 +994,7 @@ impl ContactList {
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
enum Header {}
|
||||
enum LeaveCallContactList {}
|
||||
|
||||
let header_style = theme
|
||||
.header_row
|
||||
@@ -980,9 +1007,9 @@ impl ContactList {
|
||||
};
|
||||
let leave_call = if section == Section::ActiveCall {
|
||||
Some(
|
||||
MouseEventHandler::<LeaveCall>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
|
||||
let style = theme.leave_call.style_for(state, false);
|
||||
Label::new("Leave Session".into(), style.text.clone())
|
||||
Label::new("Leave Call", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
@@ -1014,7 +1041,7 @@ impl ContactList {
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(text.to_string(), header_style.text.clone())
|
||||
Label::new(text, header_style.text.clone())
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
@@ -1047,9 +1074,10 @@ impl ContactList {
|
||||
let online = contact.online;
|
||||
let busy = contact.busy || calling;
|
||||
let user_id = contact.user.id;
|
||||
let github_login = contact.user.github_login.clone();
|
||||
let initial_project = project.clone();
|
||||
let mut element =
|
||||
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, _| {
|
||||
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, cx| {
|
||||
Flex::row()
|
||||
.with_children(contact.user.avatar.clone().map(|avatar| {
|
||||
let status_badge = if contact.online {
|
||||
@@ -1070,7 +1098,7 @@ impl ContactList {
|
||||
};
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -1091,9 +1119,33 @@ impl ContactList {
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel>::new(
|
||||
contact.user.id as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
let button_style =
|
||||
theme.contact_button.style_for(mouse_state, false);
|
||||
render_icon_button(button_style, "icons/x_mark_8.svg")
|
||||
.aligned()
|
||||
.flex_float()
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
})
|
||||
})
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(if calling {
|
||||
Some(
|
||||
Label::new("Calling".to_string(), theme.calling_indicator.text.clone())
|
||||
Label::new("Calling", theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.calling_indicator.container)
|
||||
.aligned()
|
||||
@@ -1142,7 +1194,7 @@ impl ContactList {
|
||||
|
||||
let mut row = Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
@@ -1162,6 +1214,7 @@ impl ContactList {
|
||||
);
|
||||
|
||||
let user_id = user.id;
|
||||
let github_login = user.github_login.clone();
|
||||
let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user);
|
||||
let button_spacing = theme.contact_button_spacing;
|
||||
|
||||
@@ -1223,7 +1276,10 @@ impl ContactList {
|
||||
.with_padding(Padding::uniform(2.))
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RemoveContact(user_id))
|
||||
cx.dispatch_action(RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
})
|
||||
})
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
@@ -1250,12 +1306,6 @@ impl ContactList {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactList {
|
||||
@@ -1267,9 +1317,9 @@ impl View for ContactList {
|
||||
"ContactList"
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
|
||||
@@ -1301,7 +1351,7 @@ impl View for ContactList {
|
||||
})
|
||||
.with_tooltip::<AddContact, _>(
|
||||
0,
|
||||
"Add contact".into(),
|
||||
"Search for new contact".into(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
|
||||
@@ -48,7 +48,7 @@ impl View for ContactNotification {
|
||||
ContactEventKind::Requested => render_user_notification(
|
||||
self.user.clone(),
|
||||
"wants to add you as a contact",
|
||||
Some("They won't know if you decline."),
|
||||
Some("They won't be alerted if you decline."),
|
||||
Dismiss(self.user.id),
|
||||
vec![
|
||||
(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleCollaborationMenu};
|
||||
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
|
||||
use client::UserStore;
|
||||
use gpui::{
|
||||
actions, elements::*, ClipboardItem, CursorStyle, Entity, ModelHandle, MouseButton,
|
||||
@@ -43,19 +43,23 @@ impl ContactsPopover {
|
||||
user_store,
|
||||
_subscription: None,
|
||||
};
|
||||
this.show_contact_list(cx);
|
||||
this.show_contact_list(String::new(), cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
|
||||
match &self.child {
|
||||
Child::ContactList(_) => self.show_contact_finder(cx),
|
||||
Child::ContactFinder(_) => self.show_contact_list(cx),
|
||||
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
|
||||
Child::ContactFinder(finder) => {
|
||||
self.show_contact_list(finder.read(cx).editor_text(cx), cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_contact_finder(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx));
|
||||
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child = cx.add_view(|cx| {
|
||||
ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx)
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
@@ -64,9 +68,11 @@ impl ContactsPopover {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn show_contact_list(&mut self, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child =
|
||||
cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx));
|
||||
fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
|
||||
let child = cx.add_view(|cx| {
|
||||
ContactList::new(self.project.clone(), self.user_store.clone(), cx)
|
||||
.with_editor_text(editor_text, cx)
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
@@ -155,7 +161,7 @@ impl View for ContactsPopover {
|
||||
.boxed()
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleCollaborationMenu);
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
101
crates/collab_ui/src/face_pile.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::ToJson,
|
||||
serde_json::{self, json},
|
||||
Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
|
||||
};
|
||||
|
||||
pub(crate) struct FacePile {
|
||||
overlap: f32,
|
||||
faces: Vec<ElementBox>,
|
||||
}
|
||||
|
||||
impl FacePile {
|
||||
pub fn new(overlap: f32) -> FacePile {
|
||||
FacePile {
|
||||
overlap,
|
||||
faces: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for FacePile {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
cx: &mut gpui::LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
let mut width = 0.;
|
||||
for face in &mut self.faces {
|
||||
width += face.layout(constraint, cx).x();
|
||||
}
|
||||
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
(Vector2F::new(width, constraint.max.y()), ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
) -> 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();
|
||||
cx.paint_layer(None, |cx| {
|
||||
face.paint(vec2f(origin_x, origin_y), visible_bounds, cx);
|
||||
});
|
||||
origin_x += self.overlap;
|
||||
}
|
||||
|
||||
()
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
&self,
|
||||
_: Range<usize>,
|
||||
_: RectF,
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &MeasurementContext,
|
||||
) -> Option<RectF> {
|
||||
None
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &DebugContext,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "FacePile",
|
||||
"bounds": bounds.to_json()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<ElementBox> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,12 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
});
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let screen_size = screen.size();
|
||||
let screen_bounds = screen.bounds();
|
||||
let (window_id, _) = cx.add_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||
screen_bounds.upper_right()
|
||||
- vec2f(PADDING + window_size.x(), PADDING),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
@@ -48,6 +49,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
},
|
||||
|_| IncomingCallNotification::new(incoming_call.clone()),
|
||||
);
|
||||
|
||||
notification_windows.push(window_id);
|
||||
}
|
||||
}
|
||||
@@ -106,7 +108,7 @@ impl IncomingCallNotification {
|
||||
.unwrap_or(&default_project);
|
||||
Flex::row()
|
||||
.with_children(self.call.calling_user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.caller_avatar)
|
||||
.aligned()
|
||||
.boxed()
|
||||
@@ -170,7 +172,7 @@ impl IncomingCallNotification {
|
||||
.with_child(
|
||||
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
Label::new("Accept".to_string(), theme.accept_button.text.clone())
|
||||
Label::new("Accept", theme.accept_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.accept_button.container)
|
||||
@@ -186,7 +188,7 @@ impl IncomingCallNotification {
|
||||
.with_child(
|
||||
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
Label::new("Decline".to_string(), theme.decline_button.text.clone())
|
||||
Label::new("Decline", theme.decline_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.decline_button.container)
|
||||
@@ -225,6 +227,7 @@ impl View for IncomingCallNotification {
|
||||
.theme
|
||||
.incoming_call_notification
|
||||
.background;
|
||||
|
||||
Flex::row()
|
||||
.with_child(self.render_caller(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
|
||||
@@ -11,8 +11,8 @@ enum Button {}
|
||||
|
||||
pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
user: Arc<User>,
|
||||
title: &str,
|
||||
body: Option<&str>,
|
||||
title: &'static str,
|
||||
body: Option<&'static str>,
|
||||
dismiss_action: A,
|
||||
buttons: Vec<(&'static str, Box<dyn Action>)>,
|
||||
cx: &mut RenderContext<V>,
|
||||
@@ -24,7 +24,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.header_avatar)
|
||||
.aligned()
|
||||
.constrained()
|
||||
@@ -83,7 +83,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
.named("contact notification header"),
|
||||
)
|
||||
.with_children(body.map(|body| {
|
||||
Label::new(body.to_string(), theme.body_message.text.clone())
|
||||
Label::new(body, theme.body_message.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.body_message.container)
|
||||
.boxed()
|
||||
@@ -97,7 +97,7 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
|(ix, (message, action))| {
|
||||
MouseEventHandler::<Button>::new(ix, cx, |state, _| {
|
||||
let button = theme.button.style_for(state, false);
|
||||
Label::new(message.to_string(), button.text.clone())
|
||||
Label::new(message, button.text.clone())
|
||||
.contained()
|
||||
.with_style(button.container)
|
||||
.boxed()
|
||||
|
||||
@@ -31,11 +31,11 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let screen_size = screen.size();
|
||||
let screen_bounds = screen.bounds();
|
||||
let (window_id, _) = cx.add_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
vec2f(screen_size.x() - window_size.x() - PADDING, PADDING),
|
||||
screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
@@ -108,7 +108,7 @@ impl ProjectSharedNotification {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
Flex::row()
|
||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.owner_avatar)
|
||||
.aligned()
|
||||
.boxed()
|
||||
@@ -175,7 +175,7 @@ impl ProjectSharedNotification {
|
||||
.with_child(
|
||||
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
Label::new("Open".to_string(), theme.open_button.text.clone())
|
||||
Label::new("Open", theme.open_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.open_button.container)
|
||||
@@ -194,7 +194,7 @@ impl ProjectSharedNotification {
|
||||
.with_child(
|
||||
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
Label::new("Dismiss".to_string(), theme.dismiss_button.text.clone())
|
||||
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.dismiss_button.container)
|
||||
|
||||
59
crates/collab_ui/src/sharing_status_indicator.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use call::ActiveCall;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{MouseEventHandler, Svg},
|
||||
Appearance, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext, View,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::ToggleScreenSharing;
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
|
||||
let mut status_indicator = None;
|
||||
cx.observe(&active_call, move |call, cx| {
|
||||
if let Some(room) = call.read(cx).room() {
|
||||
if room.read(cx).is_screen_sharing() {
|
||||
if status_indicator.is_none() && cx.global::<Settings>().show_call_status_icon {
|
||||
status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator));
|
||||
}
|
||||
} else if let Some((window_id, _)) = status_indicator.take() {
|
||||
cx.remove_status_bar_item(window_id);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct SharingStatusIndicator;
|
||||
|
||||
impl Entity for SharingStatusIndicator {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for SharingStatusIndicator {
|
||||
fn ui_name() -> &'static str {
|
||||
"SharingStatusIndicator"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
let color = match cx.appearance {
|
||||
Appearance::Light | Appearance::VibrantLight => Color::black(),
|
||||
Appearance::Dark | Appearance::VibrantDark => Color::white(),
|
||||
};
|
||||
|
||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||
.with_color(color)
|
||||
.constrained()
|
||||
.with_width(18.)
|
||||
.aligned()
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(ToggleScreenSharing);
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/collections.rs"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "command_palette"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/command_palette.rs"
|
||||
|
||||
@@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{ChildView, Flex, Label, ParentElement},
|
||||
keymap::Keystroke,
|
||||
keymap_matcher::Keystroke,
|
||||
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
@@ -64,8 +64,10 @@ impl CommandPalette {
|
||||
name: humanize_action_name(name),
|
||||
action,
|
||||
keystrokes: bindings
|
||||
.iter()
|
||||
.map(|binding| binding.keystrokes())
|
||||
.last()
|
||||
.map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
|
||||
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
@@ -255,7 +257,7 @@ impl PickerDelegate for CommandPalette {
|
||||
.filter_map(|(modifier, label)| {
|
||||
if modifier {
|
||||
Some(
|
||||
Label::new(label.into(), key_style.label.clone())
|
||||
Label::new(label, key_style.label.clone())
|
||||
.contained()
|
||||
.with_style(key_style.container)
|
||||
.boxed(),
|
||||
@@ -350,9 +352,7 @@ mod tests {
|
||||
});
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let editor = cx.add_view(&workspace, |cx| {
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
editor.set_text("abc", cx);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "context_menu"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/context_menu.rs"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use gpui::{
|
||||
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
|
||||
Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext,
|
||||
SizeConstraint, Subscription, View, ViewContext,
|
||||
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
|
||||
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
|
||||
MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
|
||||
};
|
||||
use menu::*;
|
||||
use settings::Settings;
|
||||
use std::{any::TypeId, time::Duration};
|
||||
use std::{any::TypeId, borrow::Cow, time::Duration};
|
||||
|
||||
pub type StaticItem = Box<dyn Fn(&mut MutableAppContext) -> ElementBox>;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
struct Clicked;
|
||||
@@ -24,16 +26,17 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
|
||||
pub enum ContextMenuItem {
|
||||
Item {
|
||||
label: String,
|
||||
label: Cow<'static, str>,
|
||||
action: Box<dyn Action>,
|
||||
},
|
||||
Static(StaticItem),
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
pub fn item(label: impl ToString, action: impl 'static + Action) -> Self {
|
||||
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
|
||||
Self::Item {
|
||||
label: label.to_string(),
|
||||
label: label.into(),
|
||||
action: Box::new(action),
|
||||
}
|
||||
}
|
||||
@@ -42,14 +45,14 @@ impl ContextMenuItem {
|
||||
Self::Separator
|
||||
}
|
||||
|
||||
fn is_separator(&self) -> bool {
|
||||
matches!(self, Self::Separator)
|
||||
fn is_action(&self) -> bool {
|
||||
matches!(self, Self::Item { .. })
|
||||
}
|
||||
|
||||
fn action_id(&self) -> Option<TypeId> {
|
||||
match self {
|
||||
ContextMenuItem::Item { action, .. } => Some(action.id()),
|
||||
ContextMenuItem::Separator => None,
|
||||
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,11 +61,13 @@ pub struct ContextMenu {
|
||||
show_count: usize,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
position_mode: OverlayPositionMode,
|
||||
items: Vec<ContextMenuItem>,
|
||||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
previously_focused_view_id: Option<usize>,
|
||||
clicked: bool,
|
||||
parent_view_id: usize,
|
||||
_actions_observation: Subscription,
|
||||
}
|
||||
|
||||
@@ -75,9 +80,9 @@ impl View for ContextMenu {
|
||||
"ContextMenu"
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
}
|
||||
|
||||
@@ -104,6 +109,7 @@ impl View for ContextMenu {
|
||||
.with_fit_mode(OverlayFitMode::SnapToWindow)
|
||||
.with_anchor_position(self.anchor_position)
|
||||
.with_anchor_corner(self.anchor_corner)
|
||||
.with_position_mode(self.position_mode)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -114,15 +120,19 @@ impl View for ContextMenu {
|
||||
|
||||
impl ContextMenu {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let parent_view_id = cx.parent().unwrap();
|
||||
|
||||
Self {
|
||||
show_count: 0,
|
||||
anchor_position: Default::default(),
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
position_mode: OverlayPositionMode::Window,
|
||||
items: Default::default(),
|
||||
selected_index: Default::default(),
|
||||
visible: Default::default(),
|
||||
previously_focused_view_id: Default::default(),
|
||||
clicked: false,
|
||||
parent_view_id,
|
||||
_actions_observation: cx.observe_actions(Self::action_dispatched),
|
||||
}
|
||||
}
|
||||
@@ -184,13 +194,13 @@ impl ContextMenu {
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = self.items.iter().position(|item| !item.is_separator());
|
||||
self.selected_index = self.items.iter().position(|item| item.is_action());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
|
||||
for (ix, item) in self.items.iter().enumerate().rev() {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@@ -201,7 +211,7 @@ impl ContextMenu {
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().skip(ix + 1) {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@@ -215,7 +225,7 @@ impl ContextMenu {
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
for (ix, item) in self.items.iter().enumerate().take(ix).rev() {
|
||||
if !item.is_separator() {
|
||||
if item.is_action() {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
break;
|
||||
@@ -230,7 +240,7 @@ impl ContextMenu {
|
||||
&mut self,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
items: impl IntoIterator<Item = ContextMenuItem>,
|
||||
items: Vec<ContextMenuItem>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut items = items.into_iter().peekable();
|
||||
@@ -250,7 +260,12 @@ impl ContextMenu {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_position_mode(&mut self, mode: OverlayPositionMode) {
|
||||
self.position_mode = mode;
|
||||
}
|
||||
|
||||
fn render_menu_for_measurement(&self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||
let window_id = cx.window_id();
|
||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||
Flex::row()
|
||||
.with_child(
|
||||
@@ -268,6 +283,9 @@ impl ContextMenu {
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
@@ -289,12 +307,17 @@ impl ContextMenu {
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
self.parent_view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(_) => Empty::new().boxed(),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.constrained()
|
||||
@@ -318,6 +341,7 @@ impl ContextMenu {
|
||||
|
||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||
|
||||
let window_id = cx.window_id();
|
||||
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
|
||||
Flex::column()
|
||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
@@ -331,12 +355,14 @@ impl ContextMenu {
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
Label::new(label.clone(), style.label.clone())
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child({
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
self.parent_view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
@@ -356,6 +382,9 @@ impl ContextMenu {
|
||||
.on_drag(MouseButton::Left, |_, _| {})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.constrained()
|
||||
.with_height(1.)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "db"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/db.rs"
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod query;
|
||||
// Re-export
|
||||
pub use anyhow;
|
||||
use anyhow::Context;
|
||||
use gpui::MutableAppContext;
|
||||
pub use indoc::indoc;
|
||||
pub use lazy_static;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
@@ -17,6 +18,7 @@ use sqlez::domain::Migrator;
|
||||
use sqlez::thread_safe_connection::ThreadSafeConnection;
|
||||
use sqlez_macros::sql;
|
||||
use std::fs::create_dir_all;
|
||||
use std::future::Future;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -39,6 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
|
||||
const DB_FILE_NAME: &'static str = "db.sqlite";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
|
||||
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
||||
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
@@ -63,11 +66,11 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||
let connection = async_iife!({
|
||||
// Note: This still has a race condition where 1 set of migrations succeeds
|
||||
// (e.g. (Workspace, Editor)) and another fails (e.g. (Workspace, Terminal))
|
||||
// This will cause the first connection to have the database taken out
|
||||
// This will cause the first connection to have the database taken out
|
||||
// from under it. This *should* be fine though. The second dabatase failure will
|
||||
// cause errors in the log and so should be observed by developers while writing
|
||||
// soon-to-be good migrations. If user databases are corrupted, we toss them out
|
||||
// and try again from a blank. As long as running all migrations from start to end
|
||||
// and try again from a blank. As long as running all migrations from start to end
|
||||
// on a blank database is ok, this race condition will never be triggered.
|
||||
//
|
||||
// Basically: Don't ever push invalid migrations to stable or everyone will have
|
||||
@@ -85,7 +88,7 @@ pub async fn open_db<M: Migrator + 'static>(
|
||||
};
|
||||
}
|
||||
|
||||
// Take a lock in the failure case so that we move the db once per process instead
|
||||
// Take a lock in the failure case so that we move the db once per process instead
|
||||
// of potentially multiple times from different threads. This shouldn't happen in the
|
||||
// normal path
|
||||
let _lock = DB_FILE_OPERATIONS.lock();
|
||||
@@ -236,6 +239,15 @@ macro_rules! define_connection {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write_and_log<F>(cx: &mut MutableAppContext, db_write: impl FnOnce() -> F + Send + 'static)
|
||||
where
|
||||
F: Future<Output = anyhow::Result<()>> + Send,
|
||||
{
|
||||
cx.background()
|
||||
.spawn(async move { db_write().await.log_err() })
|
||||
.detach()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{fs, thread};
|
||||
@@ -327,8 +339,6 @@ mod tests {
|
||||
.path();
|
||||
corrupted_backup_dir.push(DB_FILE_NAME);
|
||||
|
||||
dbg!(&corrupted_backup_dir);
|
||||
|
||||
let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy());
|
||||
assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()()
|
||||
.unwrap()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "diagnostics"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/diagnostics.rs"
|
||||
|
||||
@@ -90,14 +90,11 @@ impl View for ProjectDiagnosticsEditor {
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
if self.path_states.is_empty() {
|
||||
let theme = &cx.global::<Settings>().theme.project_diagnostics;
|
||||
Label::new(
|
||||
"No problems in workspace".to_string(),
|
||||
theme.empty_message.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
Label::new("No problems in workspace", theme.empty_message.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
} else {
|
||||
ChildView::new(&self.editor, cx).boxed()
|
||||
}
|
||||
@@ -521,12 +518,8 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
)
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
|
||||
self.editor.project_entry_ids(cx)
|
||||
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
|
||||
self.editor.for_each_project_item(cx, f)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
@@ -584,7 +577,7 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
Editor::to_item_events(event)
|
||||
}
|
||||
|
||||
@@ -701,7 +694,7 @@ pub(crate) fn render_summary(
|
||||
theme: &theme::ProjectDiagnostics,
|
||||
) -> ElementBox {
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
Label::new("No problems".to_string(), text_style.clone()).boxed()
|
||||
Label::new("No problems", text_style.clone()).boxed()
|
||||
} else {
|
||||
let icon_width = theme.tab_icon_width;
|
||||
let icon_spacing = theme.tab_icon_spacing;
|
||||
@@ -812,15 +805,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| {
|
||||
Workspace::new(
|
||||
Default::default(),
|
||||
0,
|
||||
project.clone(),
|
||||
|_, _| unimplemented!(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
|
||||
@@ -178,14 +178,11 @@ impl View for DiagnosticIndicator {
|
||||
|
||||
if in_progress {
|
||||
element.add_child(
|
||||
Label::new(
|
||||
"Checking…".into(),
|
||||
style.diagnostic_message.default.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(item_spacing)
|
||||
.boxed(),
|
||||
Label::new("Checking…", style.diagnostic_message.default.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(item_spacing)
|
||||
.boxed(),
|
||||
);
|
||||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message_style = style.diagnostic_message.clone();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "drag_and_drop"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/drag_and_drop.rs"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
name = "editor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/editor.rs"
|
||||
@@ -16,7 +17,8 @@ test-support = [
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"tree-sitter-rust"
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
@@ -57,6 +59,7 @@ smol = "1.2"
|
||||
tree-sitter-rust = { version = "*", optional = true }
|
||||
tree-sitter-html = { version = "*", optional = true }
|
||||
tree-sitter-javascript = { version = "*", optional = true }
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
text = { path = "../text", features = ["test-support"] }
|
||||
@@ -74,4 +77,5 @@ unindent = "0.1.7"
|
||||
tree-sitter = "0.20"
|
||||
tree-sitter-rust = "0.20"
|
||||
tree-sitter-html = "0.19"
|
||||
tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" }
|
||||
tree-sitter-javascript = "0.20"
|
||||
|
||||
@@ -8,6 +8,7 @@ use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fold_map::FoldMap;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
fonts::{FontId, HighlightStyle},
|
||||
Entity, ModelContext, ModelHandle,
|
||||
};
|
||||
@@ -23,6 +24,12 @@ pub use block_map::{
|
||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FoldStatus {
|
||||
Folded,
|
||||
Foldable,
|
||||
}
|
||||
|
||||
pub trait ToDisplayPoint {
|
||||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
|
||||
}
|
||||
@@ -212,6 +219,10 @@ impl DisplayMap {
|
||||
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
|
||||
}
|
||||
|
||||
pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool {
|
||||
self.fold_map.set_ellipses_color(color)
|
||||
}
|
||||
|
||||
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.set_wrap_width(width, cx))
|
||||
@@ -337,7 +348,7 @@ impl DisplaySnapshot {
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
(0..=display_row).into_iter().rev().flat_map(|row| {
|
||||
self.blocks_snapshot
|
||||
@@ -411,6 +422,67 @@ impl DisplaySnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator of the start positions of the occurances of `target` in the `self` after `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn find_while<'a>(
|
||||
&'a self,
|
||||
from: DisplayPoint,
|
||||
target: &str,
|
||||
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
|
||||
}
|
||||
|
||||
/// Returns an iterator of the end positions of the occurances of `target` in the `self` before `from`
|
||||
/// Stops if `condition` returns false for any of the character position pairs observed.
|
||||
pub fn reverse_find_while<'a>(
|
||||
&'a self,
|
||||
from: DisplayPoint,
|
||||
target: &str,
|
||||
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
Self::find_internal(
|
||||
self.reverse_chars_at(from),
|
||||
target.chars().rev().collect(),
|
||||
condition,
|
||||
)
|
||||
}
|
||||
|
||||
fn find_internal<'a>(
|
||||
iterator: impl Iterator<Item = (char, DisplayPoint)> + 'a,
|
||||
target: Vec<char>,
|
||||
mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
|
||||
) -> impl Iterator<Item = DisplayPoint> + 'a {
|
||||
// List of partial matches with the index of the last seen character in target and the starting point of the match
|
||||
let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new();
|
||||
iterator
|
||||
.take_while(move |(ch, point)| condition(*ch, *point))
|
||||
.filter_map(move |(ch, point)| {
|
||||
if Some(&ch) == target.get(0) {
|
||||
partial_matches.push((0, point));
|
||||
}
|
||||
|
||||
let mut found = None;
|
||||
// Keep partial matches that have the correct next character
|
||||
partial_matches.retain_mut(|(match_position, match_start)| {
|
||||
if target.get(*match_position) == Some(&ch) {
|
||||
*match_position += 1;
|
||||
if *match_position == target.len() {
|
||||
found = Some(match_start.clone());
|
||||
// This match is completed. No need to keep tracking it
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
found
|
||||
})
|
||||
}
|
||||
|
||||
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
|
||||
let mut count = 0;
|
||||
let mut column = 0;
|
||||
@@ -530,6 +602,59 @@ impl DisplaySnapshot {
|
||||
self.blocks_snapshot.longest_row()
|
||||
}
|
||||
|
||||
pub fn fold_for_line(self: &Self, display_row: u32) -> Option<FoldStatus> {
|
||||
if self.is_foldable(display_row) {
|
||||
Some(FoldStatus::Foldable)
|
||||
} else if self.is_line_folded(display_row) {
|
||||
Some(FoldStatus::Folded)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_foldable(self: &Self, row: u32) -> bool {
|
||||
let max_point = self.max_point();
|
||||
if row >= max_point.row() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (start_indent, is_blank) = self.line_indent(row);
|
||||
if is_blank {
|
||||
return false;
|
||||
}
|
||||
|
||||
for display_row in next_rows(row, self) {
|
||||
let (indent, is_blank) = self.line_indent(display_row);
|
||||
if !is_blank {
|
||||
return indent > start_indent;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn foldable_range(self: &Self, row: u32) -> Option<Range<DisplayPoint>> {
|
||||
let start = DisplayPoint::new(row, self.line_len(row));
|
||||
|
||||
if self.is_foldable(row) && !self.is_line_folded(start.row()) {
|
||||
let (start_indent, _) = self.line_indent(row);
|
||||
let max_point = self.max_point();
|
||||
let mut end = None;
|
||||
|
||||
for row in next_rows(row, self) {
|
||||
let (indent, is_blank) = self.line_indent(row);
|
||||
if !is_blank && indent <= start_indent {
|
||||
end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end = end.unwrap_or(max_point);
|
||||
Some(start..end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn highlight_ranges<Tag: ?Sized + 'static>(
|
||||
&self,
|
||||
@@ -617,6 +742,24 @@ impl ToDisplayPoint for Anchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator<Item = u32> {
|
||||
let max_row = display_map.max_point().row();
|
||||
let start_row = display_row + 1;
|
||||
let mut current = None;
|
||||
std::iter::from_fn(move || {
|
||||
if current == None {
|
||||
current = Some(start_row);
|
||||
} else {
|
||||
current = Some(current.unwrap() + 1)
|
||||
}
|
||||
if current.unwrap() > max_row {
|
||||
None
|
||||
} else {
|
||||
current
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
@@ -627,7 +770,7 @@ pub mod tests {
|
||||
use smol::stream::StreamExt;
|
||||
use std::{env, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use util::test::{marked_text_ranges, sample_text};
|
||||
use util::test::{marked_text_offsets, marked_text_ranges, sample_text};
|
||||
use Bias::*;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
@@ -1106,7 +1249,7 @@ pub mod tests {
|
||||
vec![
|
||||
("fn ".to_string(), None),
|
||||
("out".to_string(), Some(Color::blue())),
|
||||
("…".to_string(), None),
|
||||
("⋯".to_string(), None),
|
||||
(" fn ".to_string(), Some(Color::red())),
|
||||
("inner".to_string(), Some(Color::blue())),
|
||||
("() {}\n}".to_string(), Some(Color::red())),
|
||||
@@ -1187,7 +1330,7 @@ pub mod tests {
|
||||
cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
|
||||
[
|
||||
("out".to_string(), Some(Color::blue())),
|
||||
("…\n".to_string(), None),
|
||||
("⋯\n".to_string(), None),
|
||||
(" \nfn ".to_string(), Some(Color::red())),
|
||||
("i\n".to_string(), Some(Color::blue()))
|
||||
]
|
||||
@@ -1418,6 +1561,32 @@ pub mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_internal() {
|
||||
assert("This is a ˇtest of find internal", "test");
|
||||
assert("Some text ˇaˇaˇaa with repeated characters", "aa");
|
||||
|
||||
fn assert(marked_text: &str, target: &str) {
|
||||
let (text, expected_offsets) = marked_text_offsets(marked_text);
|
||||
|
||||
let chars = text
|
||||
.chars()
|
||||
.enumerate()
|
||||
.map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32)));
|
||||
let target = target.chars();
|
||||
|
||||
assert_eq!(
|
||||
expected_offsets
|
||||
.into_iter()
|
||||
.map(|offset| offset as u32)
|
||||
.collect::<Vec<_>>(),
|
||||
DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true)
|
||||
.map(|point| point.column())
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn syntax_chunks<'a>(
|
||||
rows: Range<u32>,
|
||||
map: &ModelHandle<DisplayMap>,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
ToOffset,
|
||||
};
|
||||
use collections::BTreeMap;
|
||||
use gpui::fonts::HighlightStyle;
|
||||
use gpui::{color::Color, fonts::HighlightStyle};
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
@@ -133,6 +133,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
folds: self.0.folds.clone(),
|
||||
buffer_snapshot: buffer,
|
||||
version: self.0.version.load(SeqCst),
|
||||
ellipses_color: self.0.ellipses_color,
|
||||
};
|
||||
(snapshot, edits)
|
||||
}
|
||||
@@ -182,6 +183,7 @@ impl<'a> FoldMapWriter<'a> {
|
||||
folds: self.0.folds.clone(),
|
||||
buffer_snapshot: buffer,
|
||||
version: self.0.version.load(SeqCst),
|
||||
ellipses_color: self.0.ellipses_color,
|
||||
};
|
||||
(snapshot, edits)
|
||||
}
|
||||
@@ -192,6 +194,7 @@ pub struct FoldMap {
|
||||
transforms: Mutex<SumTree<Transform>>,
|
||||
folds: SumTree<Fold>,
|
||||
version: AtomicUsize,
|
||||
ellipses_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
@@ -209,6 +212,7 @@ impl FoldMap {
|
||||
},
|
||||
&(),
|
||||
)),
|
||||
ellipses_color: None,
|
||||
version: Default::default(),
|
||||
};
|
||||
|
||||
@@ -217,6 +221,7 @@ impl FoldMap {
|
||||
folds: this.folds.clone(),
|
||||
buffer_snapshot: this.buffer.lock().clone(),
|
||||
version: this.version.load(SeqCst),
|
||||
ellipses_color: None,
|
||||
};
|
||||
(this, snapshot)
|
||||
}
|
||||
@@ -233,6 +238,7 @@ impl FoldMap {
|
||||
folds: self.folds.clone(),
|
||||
buffer_snapshot: self.buffer.lock().clone(),
|
||||
version: self.version.load(SeqCst),
|
||||
ellipses_color: self.ellipses_color,
|
||||
};
|
||||
(snapshot, edits)
|
||||
}
|
||||
@@ -246,6 +252,15 @@ impl FoldMap {
|
||||
(FoldMapWriter(self), snapshot, edits)
|
||||
}
|
||||
|
||||
pub fn set_ellipses_color(&mut self, color: Color) -> bool {
|
||||
if self.ellipses_color != Some(color) {
|
||||
self.ellipses_color = Some(color);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn check_invariants(&self) {
|
||||
if cfg!(test) {
|
||||
assert_eq!(
|
||||
@@ -370,7 +385,7 @@ impl FoldMap {
|
||||
}
|
||||
|
||||
if fold.end > fold.start {
|
||||
let output_text = "…";
|
||||
let output_text = "⋯";
|
||||
new_transforms.push(
|
||||
Transform {
|
||||
summary: TransformSummary {
|
||||
@@ -477,6 +492,7 @@ pub struct FoldSnapshot {
|
||||
folds: SumTree<Fold>,
|
||||
buffer_snapshot: MultiBufferSnapshot,
|
||||
pub version: usize,
|
||||
pub ellipses_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl FoldSnapshot {
|
||||
@@ -739,6 +755,7 @@ impl FoldSnapshot {
|
||||
max_output_offset: range.end.0,
|
||||
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
|
||||
active_highlights: Default::default(),
|
||||
ellipses_color: self.ellipses_color,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1029,6 +1046,7 @@ pub struct FoldChunks<'a> {
|
||||
max_output_offset: usize,
|
||||
highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
|
||||
active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
|
||||
ellipses_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FoldChunks<'a> {
|
||||
@@ -1058,7 +1076,10 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
return Some(Chunk {
|
||||
text: output_text,
|
||||
syntax_highlight_id: None,
|
||||
highlight_style: None,
|
||||
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
|
||||
color: Some(color),
|
||||
..Default::default()
|
||||
}),
|
||||
diagnostic_severity: None,
|
||||
is_unnecessary: false,
|
||||
});
|
||||
@@ -1214,7 +1235,7 @@ mod tests {
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(2, 4)..Point::new(4, 1),
|
||||
]);
|
||||
assert_eq!(snapshot2.text(), "aa…cc…eeeee");
|
||||
assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee");
|
||||
assert_eq!(
|
||||
edits,
|
||||
&[
|
||||
@@ -1241,7 +1262,7 @@ mod tests {
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
let (snapshot3, edits) = map.read(buffer_snapshot, subscription.consume().into_inner());
|
||||
assert_eq!(snapshot3.text(), "123a…c123c…eeeee");
|
||||
assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee");
|
||||
assert_eq!(
|
||||
edits,
|
||||
&[
|
||||
@@ -1261,12 +1282,12 @@ mod tests {
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner());
|
||||
assert_eq!(snapshot4.text(), "123a…c123456eee");
|
||||
assert_eq!(snapshot4.text(), "123a⋯c123456eee");
|
||||
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
|
||||
let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot5.text(), "123a…c123456eee");
|
||||
assert_eq!(snapshot5.text(), "123a⋯c123456eee");
|
||||
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
|
||||
@@ -1287,19 +1308,19 @@ mod tests {
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![5..8]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "abcde…ijkl");
|
||||
assert_eq!(snapshot.text(), "abcde⋯ijkl");
|
||||
|
||||
// Create an fold adjacent to the start of the first fold.
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![0..1, 2..5]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "…b…ijkl");
|
||||
assert_eq!(snapshot.text(), "⋯b⋯ijkl");
|
||||
|
||||
// Create an fold adjacent to the end of the first fold.
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![11..11, 8..10]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]);
|
||||
assert_eq!(snapshot.text(), "…b…kl");
|
||||
assert_eq!(snapshot.text(), "⋯b⋯kl");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -1309,7 +1330,7 @@ mod tests {
|
||||
let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]);
|
||||
writer.fold(vec![0..2, 2..5]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "…fghijkl");
|
||||
assert_eq!(snapshot.text(), "⋯fghijkl");
|
||||
|
||||
// Edit within one of the folds.
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
|
||||
@@ -1317,7 +1338,7 @@ mod tests {
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
|
||||
assert_eq!(snapshot.text(), "12345…fghijkl");
|
||||
assert_eq!(snapshot.text(), "12345⋯fghijkl");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1334,7 +1355,7 @@ mod tests {
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "aa…eeeee");
|
||||
assert_eq!(snapshot.text(), "aa⋯eeeee");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1351,14 +1372,14 @@ mod tests {
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
]);
|
||||
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee");
|
||||
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
|
||||
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner());
|
||||
assert_eq!(snapshot.text(), "aa…eeeee");
|
||||
assert_eq!(snapshot.text(), "aa⋯eeeee");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1450,7 +1471,7 @@ mod tests {
|
||||
|
||||
let mut expected_text: String = buffer_snapshot.text().to_string();
|
||||
for fold_range in map.merged_fold_ranges().into_iter().rev() {
|
||||
expected_text.replace_range(fold_range.start..fold_range.end, "…");
|
||||
expected_text.replace_range(fold_range.start..fold_range.end, "⋯");
|
||||
}
|
||||
|
||||
assert_eq!(snapshot.text(), expected_text);
|
||||
@@ -1655,7 +1676,7 @@ mod tests {
|
||||
]);
|
||||
|
||||
let (snapshot, _) = map.read(buffer_snapshot, vec![]);
|
||||
assert_eq!(snapshot.text(), "aa…cccc\nd…eeeee\nffffff\n");
|
||||
assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n");
|
||||
assert_eq!(
|
||||
snapshot.buffer_rows(0).collect::<Vec<_>>(),
|
||||
[Some(0), Some(3), Some(5), Some(6)]
|
||||
|
||||
@@ -4,17 +4,17 @@ use super::{
|
||||
ToPoint, MAX_LINE_LEN,
|
||||
};
|
||||
use crate::{
|
||||
display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
|
||||
display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock},
|
||||
git::{diff_hunk_to_display, DisplayDiffHunk},
|
||||
hover_popover::{
|
||||
HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
HideHover, HoverAt, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
|
||||
},
|
||||
link_go_to_definition::{
|
||||
GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
|
||||
},
|
||||
mouse_context_menu::DeployMouseContextMenu,
|
||||
scroll::actions::Scroll,
|
||||
EditorStyle,
|
||||
EditorStyle, GutterHover, UnfoldAt,
|
||||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
@@ -48,6 +48,9 @@ use std::{
|
||||
ops::{DerefMut, Range},
|
||||
sync::Arc,
|
||||
};
|
||||
use workspace::item::Item;
|
||||
|
||||
enum FoldMarkers {}
|
||||
|
||||
struct SelectionLayout {
|
||||
head: DisplayPoint,
|
||||
@@ -114,6 +117,7 @@ impl EditorElement {
|
||||
fn attach_mouse_handlers(
|
||||
view: &WeakViewHandle<Editor>,
|
||||
position_map: &Arc<PositionMap>,
|
||||
has_popovers: bool,
|
||||
visible_bounds: RectF,
|
||||
text_bounds: RectF,
|
||||
gutter_bounds: RectF,
|
||||
@@ -190,6 +194,11 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
})
|
||||
.on_move_out(move |_, cx| {
|
||||
if has_popovers {
|
||||
cx.dispatch_action(HideHover);
|
||||
}
|
||||
})
|
||||
.on_scroll({
|
||||
let position_map = position_map.clone();
|
||||
move |e, cx| {
|
||||
@@ -206,6 +215,17 @@ impl EditorElement {
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
enum GutterHandlers {}
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<GutterHandlers>(view.id(), view.id() + 1, gutter_bounds).on_hover(
|
||||
|hover, cx| {
|
||||
cx.dispatch_action(GutterHover {
|
||||
hovered: hover.started,
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn mouse_down(
|
||||
@@ -394,16 +414,7 @@ impl EditorElement {
|
||||
) -> bool {
|
||||
// This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
|
||||
// Don't trigger hover popover if mouse is hovering over context menu
|
||||
let point = if text_bounds.contains_point(position) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
if point == target_point {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let point = position_to_display_point(position, text_bounds, position_map);
|
||||
|
||||
cx.dispatch_action(UpdateGoToDefinitionLink {
|
||||
point,
|
||||
@@ -412,6 +423,7 @@ impl EditorElement {
|
||||
});
|
||||
|
||||
cx.dispatch_action(HoverAt { point });
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -563,12 +575,25 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
|
||||
let mut x = bounds.width() - layout.gutter_padding;
|
||||
let mut x = 0.;
|
||||
let mut y = *row as f32 * line_height - scroll_top;
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
|
||||
y += (line_height - indicator.size().y()) / 2.;
|
||||
indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||
}
|
||||
|
||||
layout.fold_indicators.as_mut().map(|fold_indicators| {
|
||||
for (line, fold_indicator) in fold_indicators.iter_mut() {
|
||||
let mut x = bounds.width() - layout.gutter_padding;
|
||||
let mut y = *line as f32 * line_height - scroll_top;
|
||||
|
||||
x += ((layout.gutter_padding + layout.gutter_margin) - fold_indicator.size().x())
|
||||
/ 2.;
|
||||
y += (line_height - fold_indicator.size().y()) / 2.;
|
||||
|
||||
fold_indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut PaintContext) {
|
||||
@@ -670,6 +695,7 @@ impl EditorElement {
|
||||
let max_glyph_width = layout.position_map.em_width;
|
||||
let scroll_left = scroll_position.x() * max_glyph_width;
|
||||
let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
|
||||
let line_end_overshoot = 0.15 * layout.position_map.line_height;
|
||||
|
||||
cx.scene.push_layer(Some(bounds));
|
||||
|
||||
@@ -682,12 +708,54 @@ impl EditorElement {
|
||||
},
|
||||
});
|
||||
|
||||
let fold_corner_radius =
|
||||
self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
|
||||
for (id, range, color) in layout.fold_ranges.iter() {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
*color,
|
||||
fold_corner_radius,
|
||||
fold_corner_radius * 2.,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_top,
|
||||
scroll_left,
|
||||
bounds,
|
||||
cx,
|
||||
);
|
||||
|
||||
for bound in range_to_bounds(
|
||||
&range,
|
||||
content_origin,
|
||||
scroll_left,
|
||||
scroll_top,
|
||||
&layout.visible_display_row_range,
|
||||
line_end_overshoot,
|
||||
&layout.position_map,
|
||||
) {
|
||||
cx.scene.push_cursor_region(CursorRegion {
|
||||
bounds: bound,
|
||||
style: CursorStyle::PointingHand,
|
||||
});
|
||||
|
||||
let display_row = range.start.row();
|
||||
cx.scene.push_mouse_region(
|
||||
MouseRegion::new::<FoldMarkers>(self.view.id(), *id as usize, bound)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(UnfoldAt { display_row })
|
||||
})
|
||||
.with_notify_on_hover(true)
|
||||
.with_notify_on_click(true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (range, color) in &layout.highlighted_ranges {
|
||||
self.paint_highlighted_range(
|
||||
range.clone(),
|
||||
*color,
|
||||
0.,
|
||||
0.15 * layout.position_map.line_height,
|
||||
line_end_overshoot,
|
||||
layout,
|
||||
content_origin,
|
||||
scroll_top,
|
||||
@@ -698,9 +766,10 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
let mut cursors = SmallVec::<[Cursor; 32]>::new();
|
||||
let corner_radius = 0.15 * layout.position_map.line_height;
|
||||
|
||||
for (replica_id, selections) in &layout.selections {
|
||||
let selection_style = style.replica_selection_style(*replica_id);
|
||||
let corner_radius = 0.15 * layout.position_map.line_height;
|
||||
|
||||
for selection in selections {
|
||||
self.paint_highlighted_range(
|
||||
@@ -1112,6 +1181,24 @@ impl EditorElement {
|
||||
.width()
|
||||
}
|
||||
|
||||
fn get_fold_indicators(
|
||||
&self,
|
||||
is_singleton: bool,
|
||||
display_rows: Range<u32>,
|
||||
snapshot: &EditorSnapshot,
|
||||
) -> Option<Vec<(u32, FoldStatus)>> {
|
||||
is_singleton.then(|| {
|
||||
display_rows
|
||||
.into_iter()
|
||||
.filter_map(|display_row| {
|
||||
snapshot
|
||||
.fold_for_line(display_row)
|
||||
.map(|fold_status| (display_row, fold_status))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
//Folds contained in a hunk are ignored apart from shrinking visual size
|
||||
//If a fold contains any hunks then that fold line is marked as modified
|
||||
fn layout_git_gutters(
|
||||
@@ -1432,7 +1519,7 @@ impl EditorElement {
|
||||
} else {
|
||||
let text_style = self.style.text.clone();
|
||||
Flex::row()
|
||||
.with_child(Label::new("…".to_string(), text_style).boxed())
|
||||
.with_child(Label::new("⋯", text_style).boxed())
|
||||
.with_children(jump_icon)
|
||||
.contained()
|
||||
.with_padding_left(gutter_padding)
|
||||
@@ -1528,15 +1615,14 @@ impl Element for EditorElement {
|
||||
let snapshot = self.update_view(cx.app, |view, cx| {
|
||||
view.set_visible_line_count(size.y() / line_height);
|
||||
|
||||
let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
|
||||
let wrap_width = match view.soft_wrap_mode(cx) {
|
||||
SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance),
|
||||
SoftWrap::EditorWidth => {
|
||||
Some(text_width - gutter_margin - overscroll.x() - em_width)
|
||||
}
|
||||
SoftWrap::Column(column) => Some(column as f32 * em_advance),
|
||||
SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
|
||||
SoftWrap::EditorWidth => editor_width,
|
||||
SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
|
||||
};
|
||||
|
||||
if view.set_wrap_width(wrap_width, cx) {
|
||||
if view.set_wrap_width(Some(wrap_width), cx) {
|
||||
view.snapshot(cx)
|
||||
} else {
|
||||
snapshot
|
||||
@@ -1601,9 +1687,13 @@ impl Element for EditorElement {
|
||||
let mut active_rows = BTreeMap::new();
|
||||
let mut highlighted_rows = None;
|
||||
let mut highlighted_ranges = Vec::new();
|
||||
let mut fold_ranges = Vec::new();
|
||||
let mut show_scrollbars = false;
|
||||
let mut include_root = false;
|
||||
let mut is_singleton = false;
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
is_singleton = view.is_singleton(cx);
|
||||
|
||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
highlighted_rows = view.highlighted_rows();
|
||||
@@ -1611,6 +1701,19 @@ impl Element for EditorElement {
|
||||
highlighted_ranges =
|
||||
view.background_highlights_in_range(start_anchor..end_anchor, &display_map, theme);
|
||||
|
||||
fold_ranges.extend(
|
||||
snapshot
|
||||
.folds_in_range(start_anchor..end_anchor)
|
||||
.map(|anchor| {
|
||||
let start = anchor.start.to_point(&snapshot.buffer_snapshot);
|
||||
(
|
||||
start.row,
|
||||
start.to_display_point(&snapshot.display_snapshot)
|
||||
..anchor.end.to_display_point(&snapshot),
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
let mut remote_selections = HashMap::default();
|
||||
for (replica_id, line_mode, cursor_shape, selection) in display_map
|
||||
.buffer_snapshot
|
||||
@@ -1679,11 +1782,28 @@ impl Element for EditorElement {
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
|
||||
.into_iter()
|
||||
.map(|(id, fold)| {
|
||||
let color = self
|
||||
.style
|
||||
.folds
|
||||
.ellipses
|
||||
.background
|
||||
.style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize), false)
|
||||
.color;
|
||||
|
||||
(id, fold, color)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let line_number_layouts =
|
||||
self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx);
|
||||
|
||||
let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
|
||||
|
||||
let folds = self.get_fold_indicators(is_singleton, start_row..end_row, &snapshot);
|
||||
|
||||
let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
|
||||
|
||||
let mut max_visible_line_width = 0.0;
|
||||
@@ -1750,7 +1870,7 @@ impl Element for EditorElement {
|
||||
let mut code_actions_indicator = None;
|
||||
let mut hover = None;
|
||||
let mut mode = EditorMode::Full;
|
||||
cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||
let mut fold_indicators = cx.render(&self.view.upgrade(cx).unwrap(), |view, cx| {
|
||||
let newest_selection_head = view
|
||||
.selections
|
||||
.newest::<usize>(cx)
|
||||
@@ -1764,14 +1884,26 @@ impl Element for EditorElement {
|
||||
view.render_context_menu(newest_selection_head, style.clone(), cx);
|
||||
}
|
||||
|
||||
let active = matches!(view.context_menu, Some(crate::ContextMenu::CodeActions(_)));
|
||||
|
||||
code_actions_indicator = view
|
||||
.render_code_actions_indicator(&style, cx)
|
||||
.render_code_actions_indicator(&style, active, cx)
|
||||
.map(|indicator| (newest_selection_head.row(), indicator));
|
||||
}
|
||||
|
||||
let visible_rows = start_row..start_row + line_layouts.len() as u32;
|
||||
hover = view.hover_state.render(&snapshot, &style, visible_rows, cx);
|
||||
mode = view.mode;
|
||||
|
||||
view.render_fold_indicators(
|
||||
folds,
|
||||
&active_rows,
|
||||
&style,
|
||||
view.gutter_hovered,
|
||||
line_height,
|
||||
gutter_margin,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if let Some((_, context_menu)) = context_menu.as_mut() {
|
||||
@@ -1797,6 +1929,18 @@ impl Element for EditorElement {
|
||||
);
|
||||
}
|
||||
|
||||
fold_indicators.as_mut().map(|fold_indicators| {
|
||||
for (_, indicator) in fold_indicators.iter_mut() {
|
||||
indicator.layout(
|
||||
SizeConstraint::strict_along(
|
||||
Axis::Vertical,
|
||||
line_height * style.code_actions.vertical_scale,
|
||||
),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((_, hover_popovers)) = hover.as_mut() {
|
||||
for hover_popover in hover_popovers.iter_mut() {
|
||||
hover_popover.layout(
|
||||
@@ -1840,12 +1984,14 @@ impl Element for EditorElement {
|
||||
active_rows,
|
||||
highlighted_rows,
|
||||
highlighted_ranges,
|
||||
fold_ranges,
|
||||
line_number_layouts,
|
||||
display_hunks,
|
||||
blocks,
|
||||
selections,
|
||||
context_menu,
|
||||
code_actions_indicator,
|
||||
fold_indicators,
|
||||
hover_popovers: hover,
|
||||
},
|
||||
)
|
||||
@@ -1870,6 +2016,7 @@ impl Element for EditorElement {
|
||||
Self::attach_mouse_handlers(
|
||||
&self.view,
|
||||
&layout.position_map,
|
||||
layout.hover_popovers.is_some(),
|
||||
visible_bounds,
|
||||
text_bounds,
|
||||
gutter_bounds,
|
||||
@@ -1952,6 +2099,8 @@ impl Element for EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
type BufferRow = u32;
|
||||
|
||||
pub struct LayoutState {
|
||||
position_map: Arc<PositionMap>,
|
||||
gutter_size: Vector2F,
|
||||
@@ -1966,6 +2115,7 @@ pub struct LayoutState {
|
||||
display_hunks: Vec<DisplayDiffHunk>,
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
scrollbar_row_range: Range<f32>,
|
||||
show_scrollbars: bool,
|
||||
@@ -1973,6 +2123,7 @@ pub struct LayoutState {
|
||||
context_menu: Option<(DisplayPoint, ElementBox)>,
|
||||
code_actions_indicator: Option<(u32, ElementBox)>,
|
||||
hover_popovers: Option<(DisplayPoint, Vec<ElementBox>)>,
|
||||
fold_indicators: Option<Vec<(u32, ElementBox)>>,
|
||||
}
|
||||
|
||||
pub struct PositionMap {
|
||||
@@ -2271,6 +2422,75 @@ impl HighlightedRange {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn position_to_display_point(
|
||||
position: Vector2F,
|
||||
text_bounds: RectF,
|
||||
position_map: &PositionMap,
|
||||
) -> Option<DisplayPoint> {
|
||||
if text_bounds.contains_point(position) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
if point == target_point {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn range_to_bounds(
|
||||
range: &Range<DisplayPoint>,
|
||||
content_origin: Vector2F,
|
||||
scroll_left: f32,
|
||||
scroll_top: f32,
|
||||
visible_row_range: &Range<u32>,
|
||||
line_end_overshoot: f32,
|
||||
position_map: &PositionMap,
|
||||
) -> impl Iterator<Item = RectF> {
|
||||
let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new();
|
||||
|
||||
if range.start == range.end {
|
||||
return bounds.into_iter();
|
||||
}
|
||||
|
||||
let start_row = visible_row_range.start;
|
||||
let end_row = visible_row_range.end;
|
||||
|
||||
let row_range = if range.end.column() == 0 {
|
||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
|
||||
} else {
|
||||
cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
|
||||
};
|
||||
|
||||
let first_y =
|
||||
content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
|
||||
|
||||
for (idx, row) in row_range.enumerate() {
|
||||
let line_layout = &position_map.line_layouts[(row - start_row) as usize];
|
||||
|
||||
let start_x = if row == range.start.row() {
|
||||
content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
|
||||
- scroll_left
|
||||
} else {
|
||||
content_origin.x() - scroll_left
|
||||
};
|
||||
|
||||
let end_x = if row == range.end.row() {
|
||||
content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left
|
||||
} else {
|
||||
content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left
|
||||
};
|
||||
|
||||
bounds.push(RectF::from_points(
|
||||
vec2f(start_x, first_y + position_map.line_height * idx as f32),
|
||||
vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32),
|
||||
))
|
||||
}
|
||||
|
||||
bounds.into_iter()
|
||||
}
|
||||
|
||||
pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
|
||||
delta.powf(1.5) / 100.0
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
||||
let snapshot = editor.snapshot(cx);
|
||||
if let Some((opening_range, closing_range)) = snapshot
|
||||
.buffer_snapshot
|
||||
.enclosing_bracket_ranges(head..head)
|
||||
.innermost_enclosing_bracket_ranges(head..head)
|
||||
{
|
||||
editor.highlight_background::<MatchingBracketHighlight>(
|
||||
vec![
|
||||
@@ -32,11 +32,10 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use indoc::indoc;
|
||||
use language::{BracketPair, Language, LanguageConfig};
|
||||
use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
|
||||
@@ -45,20 +44,23 @@ mod tests {
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
brackets: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: false,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, Text},
|
||||
@@ -29,12 +30,16 @@ pub struct HoverAt {
|
||||
pub point: Option<DisplayPoint>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub struct HideHover;
|
||||
|
||||
actions!(editor, [Hover]);
|
||||
impl_internal_actions!(editor, [HoverAt]);
|
||||
impl_internal_actions!(editor, [HoverAt, HideHover]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(hover);
|
||||
cx.add_action(hover_at);
|
||||
cx.add_action(hide_hover);
|
||||
}
|
||||
|
||||
/// Bindable action which uses the most recent selection head to trigger a hover
|
||||
@@ -50,7 +55,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
|
||||
if let Some(point) = action.point {
|
||||
show_hover(editor, point, false, cx);
|
||||
} else {
|
||||
hide_hover(editor, cx);
|
||||
hide_hover(editor, &HideHover, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +63,7 @@ pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Edit
|
||||
/// Hides the type information popup.
|
||||
/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
|
||||
/// selections changed.
|
||||
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
pub fn hide_hover(editor: &mut Editor, _: &HideHover, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let did_hide = editor.hover_state.info_popover.take().is_some()
|
||||
| editor.hover_state.diagnostic_popover.take().is_some();
|
||||
|
||||
@@ -67,6 +72,10 @@ pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
|
||||
editor.clear_background_highlights::<HoverState>(cx);
|
||||
|
||||
if did_hide {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
did_hide
|
||||
}
|
||||
|
||||
@@ -121,7 +130,7 @@ fn show_hover(
|
||||
// Hover triggered from same location as last time. Don't show again.
|
||||
return;
|
||||
} else {
|
||||
hide_hover(editor, cx);
|
||||
hide_hover(editor, &HideHover, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,12 +328,10 @@ impl InfoPopover {
|
||||
MouseEventHandler::<InfoPopover>::new(0, cx, |_, cx| {
|
||||
let mut flex = Flex::new(Axis::Vertical).scrollable::<HoverBlock, _>(1, None, cx);
|
||||
flex.extend(self.contents.iter().map(|content| {
|
||||
let project = self.project.read(cx);
|
||||
if let Some(language) = content
|
||||
.language
|
||||
.clone()
|
||||
.and_then(|language| project.languages().get_language(&language))
|
||||
{
|
||||
let languages = self.project.read(cx).languages();
|
||||
if let Some(language) = content.language.clone().and_then(|language| {
|
||||
languages.language_for_name(&language).now_or_never()?.ok()
|
||||
}) {
|
||||
let runs = language
|
||||
.highlight_text(&content.text.as_str().into(), 0..content.text.len());
|
||||
|
||||
|
||||
@@ -2,19 +2,19 @@ use crate::{
|
||||
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
|
||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
FORMAT_TIMEOUT,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext,
|
||||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::proto::serialize_anchor as serialize_text_anchor;
|
||||
use language::{Bias, Buffer, File as _, OffsetRangeExt, Point, SelectionGoal};
|
||||
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
|
||||
use language::{
|
||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
|
||||
SelectionGoal,
|
||||
};
|
||||
use project::{FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
@@ -529,7 +529,7 @@ impl Item for Editor {
|
||||
) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(self.title(cx).into(), style.label.clone())
|
||||
Label::new(self.title(cx).to_string(), style.label.clone())
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
@@ -554,22 +554,10 @@ impl Item for Editor {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
let buffer = self.buffer.read(cx).as_singleton()?;
|
||||
let file = buffer.read(cx).file();
|
||||
File::from_dyn(file).map(|file| ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
|
||||
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.files(cx)
|
||||
.into_iter()
|
||||
.filter_map(|file| File::from_dyn(Some(file))?.project_entry_id(cx))
|
||||
.collect()
|
||||
.for_each_buffer(|buffer| f(buffer.id(), buffer.read(cx)));
|
||||
}
|
||||
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool {
|
||||
@@ -606,7 +594,12 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn can_save(&self, cx: &AppContext) -> bool {
|
||||
!self.buffer().read(cx).is_singleton() || self.project_path(cx).is_some()
|
||||
let buffer = &self.buffer().read(cx);
|
||||
if let Some(buffer) = buffer.as_singleton() {
|
||||
buffer.read(cx).project_path(cx).is_some()
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn save(
|
||||
@@ -615,33 +608,38 @@ impl Item for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_event("save editor", cx);
|
||||
let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
|
||||
let buffers = self.buffer().clone().read(cx).all_buffers();
|
||||
cx.as_mut().spawn(|mut cx| async move {
|
||||
format.await?;
|
||||
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||
let format = project.update(cx, |project, cx| {
|
||||
project.format(buffers, true, FormatTrigger::Save, cx)
|
||||
});
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let transaction = futures::select_biased! {
|
||||
_ = timeout => {
|
||||
log::warn!("timed out waiting for formatting");
|
||||
None
|
||||
if buffers.len() == 1 {
|
||||
project
|
||||
.update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
|
||||
.await?;
|
||||
} else {
|
||||
// For multi-buffers, only save those ones that contain changes. For clean buffers
|
||||
// we simulate saving by calling `Buffer::did_save`, so that language servers or
|
||||
// other downstream listeners of save events get notified.
|
||||
let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
|
||||
buffer.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict())
|
||||
});
|
||||
|
||||
project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.save_buffers(dirty_buffers, cx)
|
||||
})
|
||||
.await?;
|
||||
for buffer in clean_buffers {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
let version = buffer.saved_version().clone();
|
||||
let fingerprint = buffer.saved_version_fingerprint();
|
||||
let mtime = buffer.saved_mtime();
|
||||
buffer.did_save(version, fingerprint, mtime, cx);
|
||||
});
|
||||
}
|
||||
transaction = format.log_err().fuse() => transaction,
|
||||
};
|
||||
}
|
||||
|
||||
buffer
|
||||
.update(&mut cx, |buffer, cx| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0);
|
||||
}
|
||||
}
|
||||
|
||||
buffer.save(cx)
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -699,8 +697,8 @@ impl Item for Editor {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> Vec<ItemEvent> {
|
||||
let mut result = Vec::new();
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
let mut result = SmallVec::new();
|
||||
match event {
|
||||
Event::Closed => result.push(ItemEvent::CloseItem),
|
||||
Event::Saved | Event::TitleChanged => {
|
||||
@@ -765,6 +763,7 @@ impl Item for Editor {
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
let workspace_id = workspace.database_id();
|
||||
let item_id = cx.view_id();
|
||||
self.workspace_id = Some(workspace_id);
|
||||
|
||||
fn serialize(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
@@ -836,7 +835,11 @@ impl Item for Editor {
|
||||
.context("Project item at stored path was not a buffer")?;
|
||||
|
||||
Ok(cx.update(|cx| {
|
||||
cx.add_view(pane, |cx| Editor::for_buffer(buffer, Some(project), cx))
|
||||
cx.add_view(pane, |cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
})
|
||||
}))
|
||||
})
|
||||
})
|
||||
@@ -908,7 +911,7 @@ impl SearchableItem for Editor {
|
||||
matches: Vec<Range<Anchor>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.unfold_ranges([matches[index].clone()], false, cx);
|
||||
self.unfold_ranges([matches[index].clone()], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([matches[index].clone()])
|
||||
});
|
||||
@@ -1095,7 +1098,7 @@ impl StatusItemView for CursorPosition {
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
|
||||
self.update_position(editor, cx);
|
||||
} else {
|
||||
@@ -1114,7 +1117,7 @@ fn path_for_buffer<'a>(
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
|
||||
path_for_file(file, height, include_filename, cx)
|
||||
path_for_file(file.as_ref(), height, include_filename, cx)
|
||||
}
|
||||
|
||||
fn path_for_file<'a>(
|
||||
@@ -1162,6 +1165,7 @@ mod tests {
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1191,7 +1195,7 @@ mod tests {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn mtime(&self) -> std::time::SystemTime {
|
||||
fn mtime(&self) -> SystemTime {
|
||||
todo!()
|
||||
}
|
||||
|
||||
@@ -1203,17 +1207,6 @@ mod tests {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
_: u64,
|
||||
_: language::Rope,
|
||||
_: clock::Global,
|
||||
_: project::LineEnding,
|
||||
_: &mut MutableAppContext,
|
||||
) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
}
|
||||
|
||||