Compare commits
946 Commits
v0.82.3-pr
...
v0.89.2-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c71e4b749 | ||
|
|
32ea881be7 | ||
|
|
030968ec51 | ||
|
|
0c7973d274 | ||
|
|
0071a4598a | ||
|
|
d7e4544638 | ||
|
|
432d407539 | ||
|
|
6f4fd74f43 | ||
|
|
bf2016adf5 | ||
|
|
27ef0e2b52 | ||
|
|
45b42c512d | ||
|
|
705e36827c | ||
|
|
b875d4ed40 | ||
|
|
ed0b9acb0a | ||
|
|
ef80b539d1 | ||
|
|
62660f2766 | ||
|
|
193474a346 | ||
|
|
c48fed26bd | ||
|
|
3f6aa94a5f | ||
|
|
893615236d | ||
|
|
1f72f9e18b | ||
|
|
a10933c063 | ||
|
|
ac41564a8f | ||
|
|
c6672dbac2 | ||
|
|
76927b6d95 | ||
|
|
c9820fde61 | ||
|
|
584e5f7958 | ||
|
|
0122cd61c5 | ||
|
|
685e8d7007 | ||
|
|
9c707eff27 | ||
|
|
e80ab5f096 | ||
|
|
5e4a9abd09 | ||
|
|
5065804388 | ||
|
|
8f0fc918e9 | ||
|
|
9c9af5ed94 | ||
|
|
efcf78cbe9 | ||
|
|
e9ce85ebc0 | ||
|
|
501f9ab2c6 | ||
|
|
1a23fe91b4 | ||
|
|
84f98f13c4 | ||
|
|
31516b7863 | ||
|
|
51c7078f47 | ||
|
|
4568f80f2e | ||
|
|
14efc18eca | ||
|
|
05d1dd6b11 | ||
|
|
ac63043c27 | ||
|
|
0d62e76cd6 | ||
|
|
1fc9103b61 | ||
|
|
bd494037bb | ||
|
|
2269c19169 | ||
|
|
8a851ad7f8 | ||
|
|
063613bf4e | ||
|
|
88eb2b2163 | ||
|
|
6d3464fd1f | ||
|
|
edf8e276af | ||
|
|
d593377e45 | ||
|
|
cf75b090d3 | ||
|
|
986b02e217 | ||
|
|
fdad1adaf6 | ||
|
|
e4530471de | ||
|
|
1f42bfc1bd | ||
|
|
2db57b5139 | ||
|
|
02b95ef320 | ||
|
|
9cfe39f1aa | ||
|
|
5fda9e934d | ||
|
|
3a3c1c5a5b | ||
|
|
6628c4df28 | ||
|
|
59bfd40679 | ||
|
|
f890eefdef | ||
|
|
cf2bbfc85a | ||
|
|
508533ebb7 | ||
|
|
2fdc960704 | ||
|
|
b75c27da6f | ||
|
|
364631a155 | ||
|
|
128c19875d | ||
|
|
5501dd741c | ||
|
|
7900d2a20a | ||
|
|
5e39ba596e | ||
|
|
f40c498491 | ||
|
|
8d662edb6c | ||
|
|
cede296b04 | ||
|
|
54421b11f3 | ||
|
|
ae3bdd755e | ||
|
|
739d5ca373 | ||
|
|
24098c561d | ||
|
|
4185a178e7 | ||
|
|
f549ada54f | ||
|
|
aa8c0475b1 | ||
|
|
e2ff829f98 | ||
|
|
414b97adce | ||
|
|
dba7ec4a35 | ||
|
|
c2f3ac24a4 | ||
|
|
7e1ea8f274 | ||
|
|
aa58d0fd77 | ||
|
|
f812151840 | ||
|
|
eca6d2b597 | ||
|
|
58a56bdda2 | ||
|
|
d1f4b60fa1 | ||
|
|
b9dabb165e | ||
|
|
3327e8a6dd | ||
|
|
f6d7b3d2e8 | ||
|
|
793486b2e8 | ||
|
|
3f79b0c7cc | ||
|
|
cc3186cb4a | ||
|
|
0b7d095c0a | ||
|
|
37d35db3d7 | ||
|
|
26051de160 | ||
|
|
a1a66b8da4 | ||
|
|
747322a02d | ||
|
|
66b3be8687 | ||
|
|
d495c1b804 | ||
|
|
35f9996a4f | ||
|
|
ba79a8ba94 | ||
|
|
50e1e17d09 | ||
|
|
051c7566cc | ||
|
|
8abaf66602 | ||
|
|
6368ade1da | ||
|
|
8832248bb9 | ||
|
|
f67a22828b | ||
|
|
0cf1632d39 | ||
|
|
50cfe5eec3 | ||
|
|
7534c4e670 | ||
|
|
2a8e0824a6 | ||
|
|
8f49552121 | ||
|
|
e5fd953b4f | ||
|
|
1a353ad25d | ||
|
|
9575ffc1e3 | ||
|
|
168ff99927 | ||
|
|
e463e3b2e8 | ||
|
|
e6857866d4 | ||
|
|
637ed79603 | ||
|
|
65fc50745b | ||
|
|
d934da1905 | ||
|
|
ba7659fa77 | ||
|
|
2457c55fe7 | ||
|
|
aac83ceea7 | ||
|
|
7be41e19f7 | ||
|
|
ad866c5b21 | ||
|
|
774530dd05 | ||
|
|
073967c80b | ||
|
|
5362e7d346 | ||
|
|
394e87d17c | ||
|
|
dcc4dd7eb8 | ||
|
|
19b817e48a | ||
|
|
208ff2fba7 | ||
|
|
0de5a444d3 | ||
|
|
234dbc3ca9 | ||
|
|
c4d88bc529 | ||
|
|
30de64845f | ||
|
|
a5b5de5651 | ||
|
|
a37b1b6a45 | ||
|
|
b19a712799 | ||
|
|
da96802bf0 | ||
|
|
7be8dead07 | ||
|
|
7e6cccfa3d | ||
|
|
049b72e3ac | ||
|
|
54c04a6618 | ||
|
|
9307719a4b | ||
|
|
51d94f532b | ||
|
|
fc82489128 | ||
|
|
1b5be04f76 | ||
|
|
e4454f1e7f | ||
|
|
5c31c84c90 | ||
|
|
fb2f17e5c9 | ||
|
|
2d1c4a1971 | ||
|
|
dcf4791182 | ||
|
|
9bec74f1d4 | ||
|
|
9a47415cb5 | ||
|
|
5dfb0e3691 | ||
|
|
11eb9b17c9 | ||
|
|
4c9d24da7c | ||
|
|
96224fa7e8 | ||
|
|
0e332b5f94 | ||
|
|
7689cdf3f9 | ||
|
|
1d07cff765 | ||
|
|
dcb987ba9d | ||
|
|
cfdf9198da | ||
|
|
687ccd4c6f | ||
|
|
366f13bb5c | ||
|
|
49566e5677 | ||
|
|
9c32b774aa | ||
|
|
bafc1d922e | ||
|
|
e59c8e9d61 | ||
|
|
e129ed2d91 | ||
|
|
04355215b2 | ||
|
|
a69144911f | ||
|
|
85266131cf | ||
|
|
6f4b6eec5b | ||
|
|
6264b24788 | ||
|
|
8669dcdc81 | ||
|
|
f22067b236 | ||
|
|
33f6c56b14 | ||
|
|
6f39d49b18 | ||
|
|
048498e39b | ||
|
|
ef81813d56 | ||
|
|
a355b4c135 | ||
|
|
af73c1af06 | ||
|
|
3ca95678f1 | ||
|
|
9e46b17149 | ||
|
|
2200a22c07 | ||
|
|
75a0742c90 | ||
|
|
5b7e852903 | ||
|
|
deaf60005c | ||
|
|
b80cb73745 | ||
|
|
4aa2858b2b | ||
|
|
21ada545b0 | ||
|
|
44903bc193 | ||
|
|
185a624b99 | ||
|
|
10e947cb5f | ||
|
|
7190840081 | ||
|
|
146809eef0 | ||
|
|
2098ac2c77 | ||
|
|
cb1b64e51b | ||
|
|
f2a74017df | ||
|
|
c701901c7b | ||
|
|
d5cf595761 | ||
|
|
986eafd84e | ||
|
|
912fd23006 | ||
|
|
35a9b90528 | ||
|
|
bbb68c523c | ||
|
|
e32233c826 | ||
|
|
43e301eeef | ||
|
|
fb11c3e4bf | ||
|
|
1474429271 | ||
|
|
c795c9b844 | ||
|
|
623a177fe6 | ||
|
|
560160b100 | ||
|
|
2a41a32aac | ||
|
|
d7d29472c2 | ||
|
|
06a89d9104 | ||
|
|
7ae642b9b8 | ||
|
|
c193b0b8fc | ||
|
|
32c7157906 | ||
|
|
6359333749 | ||
|
|
065f71d671 | ||
|
|
7d1833b759 | ||
|
|
844b8d9e1e | ||
|
|
2c8fffc4f8 | ||
|
|
01a3e49946 | ||
|
|
9f157bdb67 | ||
|
|
729a93db6b | ||
|
|
6792788216 | ||
|
|
5a8fb18c20 | ||
|
|
5ff49bde31 | ||
|
|
d480555ec9 | ||
|
|
847d1e73a3 | ||
|
|
459cc9c959 | ||
|
|
583b15badc | ||
|
|
e49281699c | ||
|
|
924ec961ff | ||
|
|
3d6b728364 | ||
|
|
3984cc6d39 | ||
|
|
4bda5c4d69 | ||
|
|
34b0d6200f | ||
|
|
ffe35d171c | ||
|
|
58f704abcb | ||
|
|
87e206c451 | ||
|
|
99fcf23798 | ||
|
|
95a7d69bce | ||
|
|
2883d6f1ef | ||
|
|
2a11a89827 | ||
|
|
55b241a4f6 | ||
|
|
c71b59b248 | ||
|
|
923be1892d | ||
|
|
2ec994dfcd | ||
|
|
201d513c50 | ||
|
|
711d2c6fe7 | ||
|
|
bd29812391 | ||
|
|
5ab1ae1521 | ||
|
|
83f8981308 | ||
|
|
71ad7e7612 | ||
|
|
f2ad17dbc0 | ||
|
|
263afc75a8 | ||
|
|
5ab4eab369 | ||
|
|
89d8bb1425 | ||
|
|
5c437e2098 | ||
|
|
a9b107f155 | ||
|
|
d23fc6f135 | ||
|
|
a56793c214 | ||
|
|
823e15d85a | ||
|
|
667f476f7f | ||
|
|
42eca3048f | ||
|
|
258723566f | ||
|
|
2d5f03e148 | ||
|
|
1f0e79ee9d | ||
|
|
67a25126d4 | ||
|
|
5ef03992c5 | ||
|
|
f4e99ecde4 | ||
|
|
88bc55ddf5 | ||
|
|
6cf439e734 | ||
|
|
ae3394f4de | ||
|
|
1c9a7a7ad8 | ||
|
|
aa89632286 | ||
|
|
5c729c0e56 | ||
|
|
7d11329e80 | ||
|
|
9e5983305b | ||
|
|
89204e85c0 | ||
|
|
cb0c613da2 | ||
|
|
d2ba18eae9 | ||
|
|
4898417617 | ||
|
|
05fb051924 | ||
|
|
f097444546 | ||
|
|
747fbfadeb | ||
|
|
981129ef8e | ||
|
|
6403bb86e1 | ||
|
|
cbd4771f10 | ||
|
|
65e3713d4d | ||
|
|
39618ae32d | ||
|
|
0a0769d4b9 | ||
|
|
d61b12a05b | ||
|
|
362f56d519 | ||
|
|
c27859871f | ||
|
|
2e27f26339 | ||
|
|
ffd503951b | ||
|
|
55950e52c2 | ||
|
|
685f3de796 | ||
|
|
5d4fc99750 | ||
|
|
be7a58b508 | ||
|
|
0c6f103899 | ||
|
|
106064c734 | ||
|
|
628558aa39 | ||
|
|
d719352152 | ||
|
|
89fe5c6b09 | ||
|
|
477bc8da05 | ||
|
|
e5bca9c871 | ||
|
|
e9606982e6 | ||
|
|
0db7f4202a | ||
|
|
54c1e77aff | ||
|
|
3eea2fb5f8 | ||
|
|
9de4a1b70f | ||
|
|
afe75e8cbd | ||
|
|
6976d60bfe | ||
|
|
16cab5d021 | ||
|
|
8b63caa0bd | ||
|
|
f50240181a | ||
|
|
f87ae6032e | ||
|
|
2d4b2e0844 | ||
|
|
903eed964a | ||
|
|
421db9225a | ||
|
|
adf361b374 | ||
|
|
a7fc07a8cd | ||
|
|
f50afefed3 | ||
|
|
a6a2f93607 | ||
|
|
ead9ac6f23 | ||
|
|
606d5e36e1 | ||
|
|
790223f23a | ||
|
|
e4d509adf4 | ||
|
|
4d40aa5d6f | ||
|
|
f59256f761 | ||
|
|
68078853b7 | ||
|
|
307dd2b83e | ||
|
|
1e4ab6cd75 | ||
|
|
6c26f3d0e4 | ||
|
|
2b18975cdc | ||
|
|
17f138906e | ||
|
|
6a7d718381 | ||
|
|
cf53b2ca87 | ||
|
|
c03e470fe6 | ||
|
|
18e0ee44a6 | ||
|
|
93705cbe55 | ||
|
|
5465948f20 | ||
|
|
4f36ba3b1e | ||
|
|
fa32adecd5 | ||
|
|
ba50b35de6 | ||
|
|
db87e83bad | ||
|
|
a6a4b846bc | ||
|
|
5e2aaf45a0 | ||
|
|
3534665e2b | ||
|
|
04041af78b | ||
|
|
62c445da57 | ||
|
|
41bef2e444 | ||
|
|
6c60853842 | ||
|
|
bd795d7607 | ||
|
|
e1c1100c7b | ||
|
|
b70c874a0e | ||
|
|
ad7ed56e6b | ||
|
|
4663ac8abf | ||
|
|
e71846c653 | ||
|
|
deac8a6ff9 | ||
|
|
60320c6b09 | ||
|
|
54e9e7c35b | ||
|
|
214354b4da | ||
|
|
6ef0f70528 | ||
|
|
ee3637216e | ||
|
|
89352a2bdc | ||
|
|
9ae10a5dd9 | ||
|
|
defc9c8591 | ||
|
|
5fe8b73f04 | ||
|
|
d526fa6f1f | ||
|
|
d538994c7f | ||
|
|
72655fc41d | ||
|
|
6f87f9c51f | ||
|
|
1bb34e08bb | ||
|
|
dfb6a2f7fc | ||
|
|
5b2ee63f80 | ||
|
|
f12dffa60c | ||
|
|
5accf7cf4e | ||
|
|
bebb5456c7 | ||
|
|
3550110e57 | ||
|
|
191ac86f09 | ||
|
|
0ab94551f4 | ||
|
|
0f34af50a8 | ||
|
|
adfbbf21b2 | ||
|
|
f5c633e80c | ||
|
|
fca3bb3b93 | ||
|
|
9800a149a6 | ||
|
|
f55ca7ae3c | ||
|
|
18becabfa5 | ||
|
|
bc5b78198a | ||
|
|
c7166fde3b | ||
|
|
65d4c4f6ed | ||
|
|
e20eaca595 | ||
|
|
2b80dfa81d | ||
|
|
00b345fdfe | ||
|
|
23a19d85b8 | ||
|
|
0082d68d4a | ||
|
|
21e1bdc8cd | ||
|
|
6b4242cded | ||
|
|
f935047ff2 | ||
|
|
94a0de4c9f | ||
|
|
a58a33fc93 | ||
|
|
18cec8d64f | ||
|
|
e98507d8bf | ||
|
|
93f57430da | ||
|
|
bd98f78101 | ||
|
|
67491632cb | ||
|
|
7169f5c760 | ||
|
|
cee7edabf9 | ||
|
|
6385e51957 | ||
|
|
9405b49957 | ||
|
|
68867fe2e1 | ||
|
|
aa6ea920e2 | ||
|
|
9b06be2aa2 | ||
|
|
926d7b356d | ||
|
|
b6b2c5d1d1 | ||
|
|
9a6a2d9d27 | ||
|
|
316f791a77 | ||
|
|
24e06334d0 | ||
|
|
5de9652a22 | ||
|
|
5549669316 | ||
|
|
c7fcc031eb | ||
|
|
cc21421ea8 | ||
|
|
0dce5ba7ae | ||
|
|
0ccb4a50e6 | ||
|
|
e507eadb4b | ||
|
|
9b9d53fcf8 | ||
|
|
cdcb7c8084 | ||
|
|
f28eee88b6 | ||
|
|
8fa379bbc5 | ||
|
|
6e3ce6332a | ||
|
|
eec60556ab | ||
|
|
dfdf7e4866 | ||
|
|
80fc1bc276 | ||
|
|
0e31d13a1e | ||
|
|
3da55c14a6 | ||
|
|
6fb8679184 | ||
|
|
13296d502c | ||
|
|
b5abac6af6 | ||
|
|
915154b047 | ||
|
|
3115c8381d | ||
|
|
1b5e79251c | ||
|
|
9173916ab8 | ||
|
|
f554877ff4 | ||
|
|
ba248244ec | ||
|
|
6a7feb4c4c | ||
|
|
0d78266ddb | ||
|
|
6d16d2224d | ||
|
|
5e8b7bd06d | ||
|
|
26d80eef0a | ||
|
|
0214228689 | ||
|
|
6645323f1b | ||
|
|
6dfb48dbd5 | ||
|
|
8d561d6408 | ||
|
|
8f12489937 | ||
|
|
45df09245b | ||
|
|
37d3ed5f5f | ||
|
|
641f5d1107 | ||
|
|
2bc7be9a76 | ||
|
|
48ad3866b7 | ||
|
|
7f27d72b20 | ||
|
|
5c859da457 | ||
|
|
634b699281 | ||
|
|
ad7f32d7d2 | ||
|
|
26fe7a81f3 | ||
|
|
e6be35c9a5 | ||
|
|
02066afb0e | ||
|
|
506f978c41 | ||
|
|
916612caf1 | ||
|
|
6ddc610fa2 | ||
|
|
1ddbda5095 | ||
|
|
03f8c1206a | ||
|
|
1919a826f9 | ||
|
|
2d7cfb8c7c | ||
|
|
fa049bea6e | ||
|
|
49335d017a | ||
|
|
9b2d3fcd48 | ||
|
|
8fd0c9fb0e | ||
|
|
1d66f24f23 | ||
|
|
9366a0dbee | ||
|
|
f28419cfd1 | ||
|
|
712fb5ad7f | ||
|
|
1a9afd186b | ||
|
|
15d2f19b4a | ||
|
|
d2279674a7 | ||
|
|
62e763d0d3 | ||
|
|
f9e4464658 | ||
|
|
2c2076bd77 | ||
|
|
ab952f1b31 | ||
|
|
d8dac07408 | ||
|
|
270147d20c | ||
|
|
53569ece03 | ||
|
|
b6d6f5c650 | ||
|
|
8bde496e74 | ||
|
|
5302c256a4 | ||
|
|
8301ee43d6 | ||
|
|
2fe5bf419b | ||
|
|
c6d7ed33c2 | ||
|
|
ca4da52e39 | ||
|
|
e057b0193f | ||
|
|
797d47a08c | ||
|
|
92a222aba8 | ||
|
|
8f0aa3c6d9 | ||
|
|
d34ec462f8 | ||
|
|
ffd9d4eb59 | ||
|
|
3570810516 | ||
|
|
26afd592c5 | ||
|
|
5b4e58d1de | ||
|
|
023d665fb3 | ||
|
|
ae890212e3 | ||
|
|
bcf608e9e9 | ||
|
|
563f13925f | ||
|
|
a58d3d8128 | ||
|
|
bb93447a0d | ||
|
|
2cf928c85a | ||
|
|
39bddfc7b7 | ||
|
|
98ff18c430 | ||
|
|
e6489e999d | ||
|
|
d2b2dc39d9 | ||
|
|
ab6b3adb2b | ||
|
|
fb3ef4bcf6 | ||
|
|
075bab2ea9 | ||
|
|
706f6f495a | ||
|
|
ec725fe399 | ||
|
|
95bcd19020 | ||
|
|
4aaf44df94 | ||
|
|
1eeeec157e | ||
|
|
714734d279 | ||
|
|
2d8c88ad73 | ||
|
|
f0a88b3337 | ||
|
|
ad731ea6d2 | ||
|
|
4f8607039c | ||
|
|
cf304a0edc | ||
|
|
332b364a30 | ||
|
|
235470bbfd | ||
|
|
6cb0bc89d2 | ||
|
|
0296974ab1 | ||
|
|
5e16f70067 | ||
|
|
080a1f00a3 | ||
|
|
b9ed327b94 | ||
|
|
80ad59a620 | ||
|
|
c55a4c0feb | ||
|
|
3631b3a86c | ||
|
|
89af803565 | ||
|
|
137cbaba34 | ||
|
|
eacea55aaf | ||
|
|
1883e260ce | ||
|
|
7e06062bdb | ||
|
|
8313414e1e | ||
|
|
d6b0569bed | ||
|
|
f51425d390 | ||
|
|
64e0c16baa | ||
|
|
cbae4e751b | ||
|
|
912a4cf549 | ||
|
|
0f93714d4f | ||
|
|
b1f5cfaa79 | ||
|
|
b3baebde22 | ||
|
|
da19edc3e3 | ||
|
|
121264d35a | ||
|
|
7e2a461486 | ||
|
|
5cc6304fa6 | ||
|
|
3d679ddb26 | ||
|
|
18e39ef2fa | ||
|
|
7b7a495be3 | ||
|
|
f6f18be9c3 | ||
|
|
67a3891f15 | ||
|
|
92183e0d72 | ||
|
|
053b34875b | ||
|
|
653ea3a85d | ||
|
|
c2c29d3fb6 | ||
|
|
9d41f83b1b | ||
|
|
040cc4d4c3 | ||
|
|
8eb1312deb | ||
|
|
7250754f8e | ||
|
|
9e8f852afb | ||
|
|
5157442703 | ||
|
|
c65465b0b5 | ||
|
|
e9ed40da37 | ||
|
|
7f137ed3dd | ||
|
|
7f345f8bf5 | ||
|
|
3f037e5128 | ||
|
|
376aa1235f | ||
|
|
c3cf9e3185 | ||
|
|
f576586cd7 | ||
|
|
41d4454f45 | ||
|
|
69a4fffae2 | ||
|
|
7f5afeb9fa | ||
|
|
2b95aba99c | ||
|
|
1398a12062 | ||
|
|
70f8cf4cf6 | ||
|
|
4f6939732e | ||
|
|
185c1650df | ||
|
|
9108e4151e | ||
|
|
94f1775533 | ||
|
|
e8f2d985ff | ||
|
|
f985fac6f9 | ||
|
|
484cda51cf | ||
|
|
f5278c49b0 | ||
|
|
794446bf8b | ||
|
|
4c1cba6def | ||
|
|
f7de0ad8ae | ||
|
|
c485fc86a2 | ||
|
|
f62ba2eec7 | ||
|
|
5fb9d53dd0 | ||
|
|
40ab5c1fb9 | ||
|
|
4966a4a681 | ||
|
|
a8084ad3f4 | ||
|
|
780ece551e | ||
|
|
e3b2407ebf | ||
|
|
6c931ab9da | ||
|
|
eb2cce98a7 | ||
|
|
c4472b0786 | ||
|
|
d815fc88ae | ||
|
|
029538fe21 | ||
|
|
0f44648b38 | ||
|
|
e566929d9e | ||
|
|
ae5794d911 | ||
|
|
32f26d1e9a | ||
|
|
1bf85214a4 | ||
|
|
6b0faa2d9c | ||
|
|
dc999f719b | ||
|
|
106ebeb386 | ||
|
|
489b1f6a63 | ||
|
|
1c5376a560 | ||
|
|
1d41a703ad | ||
|
|
33da9e5690 | ||
|
|
e1535735b8 | ||
|
|
f65e64829e | ||
|
|
3409ee1785 | ||
|
|
c22342e271 | ||
|
|
1f35e1dbf9 | ||
|
|
a91903c2ab | ||
|
|
264a2c1835 | ||
|
|
e6f561ce46 | ||
|
|
c04cb0286a | ||
|
|
0469e25de6 | ||
|
|
83436213ad | ||
|
|
3763b985e3 | ||
|
|
a978f3fe4f | ||
|
|
5215adbd3f | ||
|
|
5d8fcceee3 | ||
|
|
4bcba487c5 | ||
|
|
272039a858 | ||
|
|
6857426b78 | ||
|
|
71a4bc7905 | ||
|
|
d953729233 | ||
|
|
f881f9e3d8 | ||
|
|
06c01a5937 | ||
|
|
db73ba5a1a | ||
|
|
1533c17cd7 | ||
|
|
7258db7a4e | ||
|
|
6042df393b | ||
|
|
8eb9c6563a | ||
|
|
92c9de1f50 | ||
|
|
87539e7b82 | ||
|
|
66d4cb8c14 | ||
|
|
a284fae515 | ||
|
|
678c188de0 | ||
|
|
3f7533a0b4 | ||
|
|
45c7073934 | ||
|
|
30f20024c0 | ||
|
|
6cbc1dcd87 | ||
|
|
20e38d2def | ||
|
|
b6437d6d9e | ||
|
|
2950344c25 | ||
|
|
15d83d40b0 | ||
|
|
816eb06a7b | ||
|
|
32f21771a6 | ||
|
|
022368225e | ||
|
|
5521ff1b22 | ||
|
|
d3b976d044 | ||
|
|
df2f471ddf | ||
|
|
06b12bbb68 | ||
|
|
c6abb0db3a | ||
|
|
c75207c4e5 | ||
|
|
c15dadbb8c | ||
|
|
d298ce3fd3 | ||
|
|
7960067cf9 | ||
|
|
54e7464163 | ||
|
|
1bbcff543b | ||
|
|
99e82d829f | ||
|
|
a45282eb63 | ||
|
|
6317e885c7 | ||
|
|
689e878bd8 | ||
|
|
57beec6071 | ||
|
|
2b6830c798 | ||
|
|
94c2eaad23 | ||
|
|
09f7e41907 | ||
|
|
7ca412ade3 | ||
|
|
a7145021b6 | ||
|
|
3cb50ed6b7 | ||
|
|
3db67a48b5 | ||
|
|
c31a5063d0 | ||
|
|
db276a422f | ||
|
|
ebbe52e6b0 | ||
|
|
dd3f6ff4ca | ||
|
|
b76194db97 | ||
|
|
7bd51851c2 | ||
|
|
a8ddba55d8 | ||
|
|
ce34bf62fe | ||
|
|
d2ba1ec275 | ||
|
|
f3ada72785 | ||
|
|
1793c5ff6c | ||
|
|
e7cb996044 | ||
|
|
6ed7f1281f | ||
|
|
6ef6f03322 | ||
|
|
a280a93cd8 | ||
|
|
2dd4920625 | ||
|
|
abdccf7393 | ||
|
|
d82cc49f79 | ||
|
|
c12e2ac3fb | ||
|
|
c7874cf169 | ||
|
|
c165fb9be5 | ||
|
|
a6115d9330 | ||
|
|
a9417f3d2e | ||
|
|
5f500d34b2 | ||
|
|
b8fab6fde9 | ||
|
|
455d383d08 | ||
|
|
f10de10915 | ||
|
|
fa7f4974a0 | ||
|
|
733abc9ed2 | ||
|
|
616188c541 | ||
|
|
8e0d359c63 | ||
|
|
d841c3729b | ||
|
|
23932b7e6c | ||
|
|
06cb388beb | ||
|
|
e6604d1641 | ||
|
|
83bf3d071d | ||
|
|
55db28e074 | ||
|
|
5dac95c47c | ||
|
|
bce51c521a | ||
|
|
993dbf86cb | ||
|
|
09111b65d8 | ||
|
|
caf3d5c163 | ||
|
|
c1810e8ec9 | ||
|
|
fe492eacbf | ||
|
|
03619dfa55 | ||
|
|
69273648b3 | ||
|
|
b8fd6435d7 | ||
|
|
aa2af53f56 | ||
|
|
39512655aa | ||
|
|
6ee0d104d6 | ||
|
|
c9048b54c1 | ||
|
|
4ac894ffbe | ||
|
|
a4fbcbf160 | ||
|
|
4d433663bd | ||
|
|
238ebafa48 | ||
|
|
88406045f5 | ||
|
|
3992e95109 | ||
|
|
f54a289b6f | ||
|
|
a860a6cd62 | ||
|
|
8c7f821d14 | ||
|
|
c3231047ad | ||
|
|
f12746c4b7 | ||
|
|
31e906d068 | ||
|
|
65c5605e68 | ||
|
|
3c54b14c5b | ||
|
|
5b40641fde | ||
|
|
b0cbd13e7a | ||
|
|
e5192a4853 | ||
|
|
c76b9794e4 | ||
|
|
d32a7218cd | ||
|
|
c7cc5bca02 | ||
|
|
8e4cc549dc | ||
|
|
1fa52adabd | ||
|
|
c72b70d4ae | ||
|
|
f54ab73b47 | ||
|
|
dfdc826015 | ||
|
|
1b2e480e1e | ||
|
|
0bce80b6f8 | ||
|
|
137d9384b5 | ||
|
|
7b4b1d6312 | ||
|
|
abdfb5a451 | ||
|
|
3a855184bc | ||
|
|
65f7228fed | ||
|
|
b414d43ee3 | ||
|
|
dcc804783c | ||
|
|
460ea8e16c | ||
|
|
b11e239779 | ||
|
|
ad71020990 | ||
|
|
21bb13d309 | ||
|
|
32c57bcd22 | ||
|
|
960a2bc589 | ||
|
|
0ebe44bfd5 | ||
|
|
4dd917c123 | ||
|
|
c5f86bc6af | ||
|
|
9e2949e7ba | ||
|
|
c59204c5e6 | ||
|
|
26abc824a9 | ||
|
|
df94aee758 | ||
|
|
6156dbced0 | ||
|
|
bb4de47b15 | ||
|
|
2a5c0fa5f8 | ||
|
|
6e68ff5a50 | ||
|
|
ba7233f265 | ||
|
|
c1daf0fc36 | ||
|
|
ad8162fc9c | ||
|
|
f5bbb41cc2 | ||
|
|
5c8b41dd54 | ||
|
|
0d5eea8169 | ||
|
|
d9bb37c649 | ||
|
|
1d487e19f9 | ||
|
|
c52b6328b7 | ||
|
|
e282c7ad45 | ||
|
|
21e39e7523 | ||
|
|
370875b1d4 | ||
|
|
eca93c124a | ||
|
|
bed76462e2 | ||
|
|
df71a9cfae | ||
|
|
4151bd39da | ||
|
|
4d207981ae | ||
|
|
5d57167302 | ||
|
|
4c3d6c854a | ||
|
|
b9a7b70e52 | ||
|
|
34bcf6f072 | ||
|
|
672cf6b8c7 | ||
|
|
d70644618a | ||
|
|
ce8442a3d8 | ||
|
|
dd73233973 | ||
|
|
26ab774b7f | ||
|
|
f16b96cafc | ||
|
|
9b8a3e4de5 | ||
|
|
2882e0fa5b | ||
|
|
745e5e3a09 | ||
|
|
70ff4ca48f | ||
|
|
ea1c3fa7a0 | ||
|
|
8610f3acf3 | ||
|
|
0326a45a91 | ||
|
|
54a78d7024 | ||
|
|
4a9989fe38 | ||
|
|
1fd07b6fcf | ||
|
|
699b2060b3 | ||
|
|
b3b8f8532d | ||
|
|
f9c60b98c0 | ||
|
|
27a6bacab8 | ||
|
|
5514349b6b | ||
|
|
c5e56a5e45 | ||
|
|
5934e882b8 | ||
|
|
ad9fe79cf2 | ||
|
|
7cc868bc8c | ||
|
|
44d26b69ae | ||
|
|
bd7d50f339 | ||
|
|
a8b3826955 | ||
|
|
4c086a4836 | ||
|
|
721baf5746 | ||
|
|
957ab65422 | ||
|
|
614a9c8977 | ||
|
|
ae0647c3a9 | ||
|
|
304eddbbe4 | ||
|
|
9afd804062 | ||
|
|
eee39b4c5c | ||
|
|
136e599051 | ||
|
|
bcba11ba82 | ||
|
|
d03c431f9a | ||
|
|
31e6bb4fc1 | ||
|
|
1b477c9e38 | ||
|
|
d26d0ac56f | ||
|
|
75d6b6360f | ||
|
|
8f25b98e6f | ||
|
|
695973d117 | ||
|
|
516964280b | ||
|
|
485c56e3bd | ||
|
|
837866f962 | ||
|
|
4adc92b8e5 | ||
|
|
14ef0edd7f | ||
|
|
233cd80f63 | ||
|
|
5d73e646d8 | ||
|
|
1f284408a9 | ||
|
|
f5a2534c1b | ||
|
|
61f4f8aaeb | ||
|
|
493a418c91 | ||
|
|
38ab6b123f | ||
|
|
bed94455b9 | ||
|
|
1dcd4717b1 | ||
|
|
ebe57254e0 | ||
|
|
3569c61784 | ||
|
|
5c3da91e15 | ||
|
|
c329546570 | ||
|
|
253411bfd0 | ||
|
|
e655a6c767 | ||
|
|
f09e21aa93 | ||
|
|
a820862165 | ||
|
|
a8e75a9b55 | ||
|
|
060242a28a | ||
|
|
2652f65bee | ||
|
|
98dce89379 | ||
|
|
74ca223114 | ||
|
|
33bc47dbe2 | ||
|
|
183b9ef809 | ||
|
|
7394bf1cdc | ||
|
|
5666e8301e | ||
|
|
9ef79735dc | ||
|
|
c62357db02 | ||
|
|
5ea49b3ae3 | ||
|
|
bb1cfd51b8 | ||
|
|
debb694d97 | ||
|
|
c13914bda1 | ||
|
|
6a75e884c0 | ||
|
|
5f0bf5929f | ||
|
|
84d2605ccf | ||
|
|
ff774786bf | ||
|
|
4900e04ff3 | ||
|
|
0269a8699b | ||
|
|
702c4ce403 | ||
|
|
f4daeb4778 | ||
|
|
495c7acadf | ||
|
|
5ca603dbeb | ||
|
|
3d14bfd90c | ||
|
|
2d97387f49 | ||
|
|
b89c4e06be | ||
|
|
25ad635577 | ||
|
|
4cb13fb39c | ||
|
|
a25f962185 | ||
|
|
a85c2d71ad | ||
|
|
afbd275f4f | ||
|
|
40896352ff | ||
|
|
868301bedb | ||
|
|
83070a19c4 | ||
|
|
b54f08db77 | ||
|
|
d9e4136b02 | ||
|
|
e6cc132b19 | ||
|
|
e115baa60c | ||
|
|
3de8fe0f87 | ||
|
|
6638407ff9 | ||
|
|
7536645eea | ||
|
|
9d23a98157 | ||
|
|
2186de38ab |
15
.github/pull_request_template.md
vendored
15
.github/pull_request_template.md
vendored
@@ -1,9 +1,12 @@
|
||||
## Description of feature or change
|
||||
[[PR Description]]
|
||||
|
||||
## Link to related issues from zed or community
|
||||
Release Notes:
|
||||
|
||||
## Before Merging
|
||||
- N/A
|
||||
|
||||
- [ ] Does this have tests or have existing tests been updated to cover this change?
|
||||
- [ ] Have you added the necessary settings to configure this feature?
|
||||
- [ ] Has documentation been created or updated (including above changes to settings)?
|
||||
or
|
||||
|
||||
- (Added|Fixed|Improved) ... ([#<public_issue_number_if_exists>](https://github.com/zed-industries/community/issues/<public_issue_number_if_exists>)).
|
||||
|
||||
If the release notes are only intended for a specific release channel only, add `(<release_channel>-only)` to the end of the release note line.
|
||||
These will be removed by the person making the release.
|
||||
|
||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -42,6 +42,7 @@ jobs:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- test
|
||||
needs: rustfmt
|
||||
env:
|
||||
RUSTFLAGS: -D warnings
|
||||
steps:
|
||||
@@ -54,7 +55,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -62,6 +63,9 @@ jobs:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Run check
|
||||
run: cargo check --workspace
|
||||
|
||||
@@ -82,7 +86,7 @@ jobs:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- bundle
|
||||
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') }}
|
||||
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
needs: tests
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
@@ -102,7 +106,7 @@ jobs:
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
@@ -110,6 +114,9 @@ jobs:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 70
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
@@ -141,11 +148,11 @@ jobs:
|
||||
- name: Create app bundle
|
||||
run: script/bundle
|
||||
|
||||
- name: Upload app bundle to workflow run if main branch
|
||||
- name: Upload app bundle to workflow run if main branch or specifi label
|
||||
uses: actions/upload-artifact@v2
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }}
|
||||
with:
|
||||
name: Zed.dmg
|
||||
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
|
||||
path: target/release/Zed.dmg
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
|
||||
43
.github/workflows/randomized_tests.yml
vendored
Normal file
43
.github/workflows/randomized_tests.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Randomized Tests
|
||||
|
||||
concurrency: randomized-tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- randomized-tests-runner
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: 1
|
||||
ZED_SERVER_URL: https://zed.dev
|
||||
ZED_CLIENT_SECRET_TOKEN: ${{ secrets.ZED_CLIENT_SECRET_TOKEN }}
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
name: Run randomized tests
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- randomized-tests
|
||||
steps:
|
||||
- name: Install Rust
|
||||
run: |
|
||||
rustup set profile minimal
|
||||
rustup update stable
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
clean: false
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Run randomized tests
|
||||
run: script/randomized-test-ci
|
||||
2
.github/workflows/release_actions.yml
vendored
2
.github/workflows/release_actions.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
content: |
|
||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
||||
|
||||
Restart your Zed or head to https://zed.dev/releases/latest to grab it.
|
||||
Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
|
||||
|
||||
```md
|
||||
# Changelog
|
||||
|
||||
1877
Cargo.lock
generated
1877
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
27
Cargo.toml
27
Cargo.toml
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/ai",
|
||||
"crates/assets",
|
||||
"crates/auto_update",
|
||||
"crates/breadcrumbs",
|
||||
@@ -35,6 +36,7 @@ members = [
|
||||
"crates/live_kit_client",
|
||||
"crates/live_kit_server",
|
||||
"crates/lsp",
|
||||
"crates/lsp_log",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/node_runtime",
|
||||
@@ -71,11 +73,32 @@ default-members = ["crates/zed"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.57" }
|
||||
async-trait = { version = "0.1" }
|
||||
ctor = { version = "0.1" }
|
||||
env_logger = { version = "0.9" }
|
||||
futures = { version = "0.3" }
|
||||
globset = { version = "0.4" }
|
||||
indoc = "1"
|
||||
isahc = "1.7.2"
|
||||
lazy_static = { version = "1.4.0" }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = { version = "2.1.1" }
|
||||
parking_lot = { version = "0.11.1" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
rand = { version = "0.8.5" }
|
||||
regex = { version = "1.5" }
|
||||
schemars = { version = "0.8" }
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
rand = { version = "0.8" }
|
||||
postage = { version = "0.5", features = ["futures-traits"] }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = { version = "1.2" }
|
||||
tempdir = { version = "0.3.7" }
|
||||
thiserror = { version = "1.0.29" }
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
toml = { version = "0.5" }
|
||||
unindent = { version = "0.1.7" }
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
|
||||
Welcome to Zed, a lightning-fast, collaborative code editor that makes your dreams come true.
|
||||
|
||||
## Development tips
|
||||
|
||||
@@ -31,7 +31,8 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
|
||||
|
||||
* 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.
|
||||
Create a personal GitHub token to run `script/bootstrap` once successfully: the token needs to have an access to private repositories for the script to work (`repo` OAuth scope).
|
||||
Then delete that token.
|
||||
|
||||
```
|
||||
GITHUB_TOKEN=<$token> script/bootstrap
|
||||
|
||||
6
assets/contexts/system.zmd
Normal file
6
assets/contexts/system.zmd
Normal file
@@ -0,0 +1,6 @@
|
||||
User input begins on a line starting with /.
|
||||
Don't apologize ever.
|
||||
Never say "I apologize".
|
||||
Use simple language and don't flatter the users.
|
||||
Keep it short.
|
||||
Risk being rude.
|
||||
3
assets/icons/version_control_branch_12.svg
Normal file
3
assets/icons/version_control_branch_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="M4.75 1.875C4.75 2.71406 4.19922 3.42422 3.4375 3.66328V5.97891C3.9086 5.64609 4.4711 5.4375 5.125 5.4375H7.375C8.30782 5.4375 9.0625 4.68281 9.0625 3.75V3.66328C8.30078 3.42422 7.75 2.71406 7.75 1.875C7.75 0.839531 8.58907 0 9.625 0C10.6609 0 11.5 0.839531 11.5 1.875C11.5 2.71406 10.9492 3.42422 10.1875 3.66328V3.75C10.1875 5.30391 8.92891 6.5625 7.375 6.5625H5.125C4.19219 6.5625 3.4375 7.31719 3.4375 8.25V8.33672C4.19922 8.57578 4.75 9.28594 4.75 10.125C4.75 11.1609 3.91094 12 2.875 12C1.83953 12 1 11.1609 1 10.125C1 9.28594 1.55172 8.57578 2.3125 8.33672V3.66328C1.55172 3.42422 1 2.71406 1 1.875C1 0.839531 1.83953 0 2.875 0C3.91094 0 4.75 0.839531 4.75 1.875ZM2.875 2.625C3.28914 2.625 3.625 2.28914 3.625 1.875C3.625 1.46086 3.28914 1.125 2.875 1.125C2.46086 1.125 2.125 1.46086 2.125 1.875C2.125 2.28914 2.46086 2.625 2.875 2.625ZM9.625 1.125C9.21016 1.125 8.875 1.46086 8.875 1.875C8.875 2.28914 9.21016 2.625 9.625 2.625C10.0398 2.625 10.375 2.28914 10.375 1.875C10.375 1.46086 10.0398 1.125 9.625 1.125ZM2.875 10.875C3.28914 10.875 3.625 10.5398 3.625 10.125C3.625 9.71016 3.28914 9.375 2.875 9.375C2.46086 9.375 2.125 9.71016 2.125 10.125C2.125 10.5398 2.46086 10.875 2.875 10.875Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -18,7 +18,9 @@
|
||||
],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
|
||||
"cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
|
||||
"cmd-shift-enter": "editor::NewlineAbove",
|
||||
"cmd-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -37,8 +39,8 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-\\": "workspace::ToggleLeftSidebar",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-\\": "workspace::ToggleLeftDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-r": "project_symbols::Toggle"
|
||||
}
|
||||
@@ -60,9 +62,5 @@
|
||||
"ctrl-f": "project_panel::ExpandSelectedEntry",
|
||||
"ctrl-shift-c": "project_panel::CopyPath"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,11 @@
|
||||
"ctrl->": "zed::IncreaseBufferFontSize",
|
||||
"ctrl-<": "zed::DecreaseBufferFontSize",
|
||||
"cmd-d": "editor::DuplicateLine",
|
||||
"cmd-backspace": "editor::DeleteLine",
|
||||
"cmd-pagedown": "editor::MovePageDown",
|
||||
"cmd-pageup": "editor::MovePageUp",
|
||||
"ctrl-alt-shift-b": "editor::SelectToPreviousWordStart",
|
||||
"cmd-alt-enter": "editor::NewlineAbove",
|
||||
"shift-enter": "editor::NewlineBelow",
|
||||
"cmd--": "editor::Fold",
|
||||
"cmd-=": "editor::UnfoldLines",
|
||||
@@ -32,6 +34,7 @@
|
||||
],
|
||||
"shift-alt-up": "editor::MoveLineUp",
|
||||
"shift-alt-down": "editor::MoveLineDown",
|
||||
"cmd-alt-l": "editor::Format",
|
||||
"cmd-[": "pane::GoBack",
|
||||
"cmd-]": "pane::GoForward",
|
||||
"alt-f7": "editor::FindAllReferences",
|
||||
@@ -62,17 +65,11 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"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"
|
||||
"cmd-1": "workspace::ToggleLeftDock",
|
||||
"cmd-6": "diagnostics::Deploy"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd",
|
||||
"cmd-shift-enter": "editor::NewlineAbove",
|
||||
"cmd-enter": "editor::NewlineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -43,18 +45,11 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"ctrl-`": "dock::FocusDock",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftSidebar",
|
||||
"cmd-k cmd-b": "workspace::ToggleLeftDock",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"ctrl-shift-d": "editor::DuplicateLine",
|
||||
"cmd-b": "editor::GoToDefinition",
|
||||
"cmd-j": "editor::ScrollCursorCenter",
|
||||
"cmd-alt-enter": "editor::NewlineAbove",
|
||||
"cmd-enter": "editor::NewlineBelow",
|
||||
"cmd-shift-l": "editor::SelectLine",
|
||||
"cmd-shift-t": "outline::Toggle",
|
||||
@@ -67,7 +68,7 @@
|
||||
{
|
||||
"context": "Workspace",
|
||||
"bindings": {
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar",
|
||||
"cmd-alt-ctrl-d": "workspace::ToggleLeftDock",
|
||||
"cmd-t": "file_finder::Toggle",
|
||||
"cmd-shift-t": "project_symbols::Toggle"
|
||||
}
|
||||
@@ -82,9 +83,5 @@
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
"bindings": {}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,325 +1,325 @@
|
||||
[
|
||||
{
|
||||
"context": "Editor && VimControl && !VimWaiting",
|
||||
"bindings": {
|
||||
"g": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Namespace": "G"
|
||||
}
|
||||
],
|
||||
"i": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"a": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"j": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"k": "vim::Up",
|
||||
"l": "vim::Right",
|
||||
"$": "vim::EndOfLine",
|
||||
"shift-g": "vim::EndOfDocument",
|
||||
"w": "vim::NextWordStart",
|
||||
"shift-w": [
|
||||
"vim::NextWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"e": "vim::NextWordEnd",
|
||||
"shift-e": [
|
||||
"vim::NextWordEnd",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"b": "vim::PreviousWordStart",
|
||||
"shift-b": [
|
||||
"vim::PreviousWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"%": "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": [
|
||||
"vim::Number",
|
||||
1
|
||||
],
|
||||
"2": [
|
||||
"vim::Number",
|
||||
2
|
||||
],
|
||||
"3": [
|
||||
"vim::Number",
|
||||
3
|
||||
],
|
||||
"4": [
|
||||
"vim::Number",
|
||||
4
|
||||
],
|
||||
"5": [
|
||||
"vim::Number",
|
||||
5
|
||||
],
|
||||
"6": [
|
||||
"vim::Number",
|
||||
6
|
||||
],
|
||||
"7": [
|
||||
"vim::Number",
|
||||
7
|
||||
],
|
||||
"8": [
|
||||
"vim::Number",
|
||||
8
|
||||
],
|
||||
"9": [
|
||||
"vim::Number",
|
||||
9
|
||||
]
|
||||
{
|
||||
"context": "Editor && VimControl && !VimWaiting",
|
||||
"bindings": {
|
||||
"g": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Namespace": "G"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"c": [
|
||||
"vim::PushOperator",
|
||||
"Change"
|
||||
],
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": [
|
||||
"vim::PushOperator",
|
||||
"Delete"
|
||||
],
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"y": [
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"z": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Namespace": "Z"
|
||||
}
|
||||
],
|
||||
"i": [
|
||||
"vim::SwitchMode",
|
||||
"Insert"
|
||||
],
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"x": "vim::DeleteRight",
|
||||
"shift-x": "vim::DeleteLeft",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
"o": "vim::InsertLineBelow",
|
||||
"shift-o": "vim::InsertLineAbove",
|
||||
"v": [
|
||||
"vim::SwitchMode",
|
||||
{
|
||||
"Visual": {
|
||||
"line": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"shift-v": [
|
||||
"vim::SwitchMode",
|
||||
{
|
||||
"Visual": {
|
||||
"line": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"p": "vim::Paste",
|
||||
"u": "editor::Undo",
|
||||
"ctrl-r": "editor::Redo",
|
||||
"ctrl-o": "pane::GoBack",
|
||||
"/": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
"ctrl-f": [
|
||||
"vim::Scroll",
|
||||
"PageDown"
|
||||
],
|
||||
"ctrl-b": [
|
||||
"vim::Scroll",
|
||||
"PageUp"
|
||||
],
|
||||
"ctrl-d": [
|
||||
"vim::Scroll",
|
||||
"HalfPageDown"
|
||||
],
|
||||
"ctrl-u": [
|
||||
"vim::Scroll",
|
||||
"HalfPageUp"
|
||||
],
|
||||
"ctrl-e": [
|
||||
"vim::Scroll",
|
||||
"LineDown"
|
||||
],
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
]
|
||||
],
|
||||
"i": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == n",
|
||||
"bindings": {
|
||||
"0": [
|
||||
"vim::Number",
|
||||
0
|
||||
]
|
||||
],
|
||||
"a": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Object": {
|
||||
"around": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == g",
|
||||
"bindings": {
|
||||
"g": "vim::StartOfDocument",
|
||||
"h": "editor::Hover",
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"d": "editor::GoToDefinition"
|
||||
],
|
||||
"h": "vim::Left",
|
||||
"backspace": "vim::Backspace",
|
||||
"j": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"k": "vim::Up",
|
||||
"l": "vim::Right",
|
||||
"$": "vim::EndOfLine",
|
||||
"shift-g": "vim::EndOfDocument",
|
||||
"w": "vim::NextWordStart",
|
||||
"shift-w": [
|
||||
"vim::NextWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == c",
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine"
|
||||
],
|
||||
"e": "vim::NextWordEnd",
|
||||
"shift-e": [
|
||||
"vim::NextWordEnd",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine"
|
||||
],
|
||||
"b": "vim::PreviousWordStart",
|
||||
"shift-b": [
|
||||
"vim::PreviousWordStart",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine"
|
||||
],
|
||||
"%": "vim::Matching",
|
||||
"ctrl-y": [
|
||||
"vim::Scroll",
|
||||
"LineUp"
|
||||
],
|
||||
"f": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindForward": {
|
||||
"before": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == z",
|
||||
"bindings": {
|
||||
"t": "editor::ScrollCursorTop",
|
||||
"z": "editor::ScrollCursorCenter",
|
||||
"b": "editor::ScrollCursorBottom",
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
],
|
||||
"t": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindForward": {
|
||||
"before": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": [
|
||||
"vim::Word",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"s": "vim::Sentence",
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"{": "vim::CurlyBrackets",
|
||||
"}": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets"
|
||||
],
|
||||
"shift-f": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindBackward": {
|
||||
"after": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimWaiting",
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": "editor::Cancel"
|
||||
],
|
||||
"shift-t": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"FindBackward": {
|
||||
"after": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"escape": "editor::Cancel",
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"1": [
|
||||
"vim::Number",
|
||||
1
|
||||
],
|
||||
"2": [
|
||||
"vim::Number",
|
||||
2
|
||||
],
|
||||
"3": [
|
||||
"vim::Number",
|
||||
3
|
||||
],
|
||||
"4": [
|
||||
"vim::Number",
|
||||
4
|
||||
],
|
||||
"5": [
|
||||
"vim::Number",
|
||||
5
|
||||
],
|
||||
"6": [
|
||||
"vim::Number",
|
||||
6
|
||||
],
|
||||
"7": [
|
||||
"vim::Number",
|
||||
7
|
||||
],
|
||||
"8": [
|
||||
"vim::Number",
|
||||
8
|
||||
],
|
||||
"9": [
|
||||
"vim::Number",
|
||||
9
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"c": [
|
||||
"vim::PushOperator",
|
||||
"Change"
|
||||
],
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": [
|
||||
"vim::PushOperator",
|
||||
"Delete"
|
||||
],
|
||||
"shift-d": "vim::DeleteToEndOfLine",
|
||||
"y": [
|
||||
"vim::PushOperator",
|
||||
"Yank"
|
||||
],
|
||||
"z": [
|
||||
"vim::PushOperator",
|
||||
{
|
||||
"Namespace": "Z"
|
||||
}
|
||||
],
|
||||
"i": [
|
||||
"vim::SwitchMode",
|
||||
"Insert"
|
||||
],
|
||||
"shift-i": "vim::InsertFirstNonWhitespace",
|
||||
"a": "vim::InsertAfter",
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"x": "vim::DeleteRight",
|
||||
"shift-x": "vim::DeleteLeft",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
"o": "vim::InsertLineBelow",
|
||||
"shift-o": "vim::InsertLineAbove",
|
||||
"v": [
|
||||
"vim::SwitchMode",
|
||||
{
|
||||
"Visual": {
|
||||
"line": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"shift-v": [
|
||||
"vim::SwitchMode",
|
||||
{
|
||||
"Visual": {
|
||||
"line": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"p": "vim::Paste",
|
||||
"u": "editor::Undo",
|
||||
"ctrl-r": "editor::Redo",
|
||||
"ctrl-o": "pane::GoBack",
|
||||
"/": [
|
||||
"buffer_search::Deploy",
|
||||
{
|
||||
"focus": true
|
||||
}
|
||||
],
|
||||
"ctrl-f": [
|
||||
"vim::Scroll",
|
||||
"PageDown"
|
||||
],
|
||||
"ctrl-b": [
|
||||
"vim::Scroll",
|
||||
"PageUp"
|
||||
],
|
||||
"ctrl-d": [
|
||||
"vim::Scroll",
|
||||
"HalfPageDown"
|
||||
],
|
||||
"ctrl-u": [
|
||||
"vim::Scroll",
|
||||
"HalfPageUp"
|
||||
],
|
||||
"ctrl-e": [
|
||||
"vim::Scroll",
|
||||
"LineDown"
|
||||
],
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == n",
|
||||
"bindings": {
|
||||
"0": [
|
||||
"vim::Number",
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == g",
|
||||
"bindings": {
|
||||
"g": "vim::StartOfDocument",
|
||||
"h": "editor::Hover",
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"d": "editor::GoToDefinition"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == c",
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == z",
|
||||
"bindings": {
|
||||
"t": "editor::ScrollCursorTop",
|
||||
"z": "editor::ScrollCursorCenter",
|
||||
"b": "editor::ScrollCursorBottom",
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": [
|
||||
"vim::Word",
|
||||
{
|
||||
"ignorePunctuation": true
|
||||
}
|
||||
],
|
||||
"s": "vim::Sentence",
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"{": "vim::CurlyBrackets",
|
||||
"}": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"r": [
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimWaiting",
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": "editor::Cancel"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,251 +1,321 @@
|
||||
{
|
||||
// The name of the Zed theme to use for the UI
|
||||
"theme": "One Dark",
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
// The OpenType features to enable for text in the editor.
|
||||
"buffer_font_features": {
|
||||
// Disable ligatures:
|
||||
// "calt": false
|
||||
},
|
||||
// The default font size for text in the editor
|
||||
"buffer_font_size": 15,
|
||||
// The factor to grow the active pane by. Defaults to 1.0
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
// Enable / disable copilot integration.
|
||||
"enable_copilot_integration": true,
|
||||
// Controls whether copilot provides suggestion immediately
|
||||
// or waits for a `copilot::Toggle`
|
||||
"copilot": "on",
|
||||
// Whether to enable vim modes and key bindings
|
||||
"vim_mode": false,
|
||||
// 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 the screen sharing icon is shown 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
|
||||
// take four values.
|
||||
//
|
||||
// 1. Never automatically save:
|
||||
// "autosave": "off",
|
||||
// 2. Save when changing focus away from the Zed window:
|
||||
// "autosave": "on_window_change",
|
||||
// 3. Save when changing focus away from a specific buffer:
|
||||
// "autosave": "on_focus_change",
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// Where to place the dock by default. This setting can take three
|
||||
// values:
|
||||
//
|
||||
// 1. Position the dock attached to the bottom of the workspace
|
||||
// "default_dock_anchor": "bottom"
|
||||
// 2. Position the dock to the right of the workspace like a side panel
|
||||
// "default_dock_anchor": "right"
|
||||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"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:
|
||||
//
|
||||
// 1. Format code using the current language server:
|
||||
// "format_on_save": "language_server"
|
||||
// 2. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// The name of the Zed theme to use for the UI
|
||||
"theme": "One Dark",
|
||||
// The name of a base set of key bindings to use.
|
||||
// This setting can take four values, each named after another
|
||||
// text editor:
|
||||
//
|
||||
// 1. "VSCode"
|
||||
// 2. "JetBrains"
|
||||
// 3. "SublimeText"
|
||||
// 4. "Atom"
|
||||
"base_keymap": "VSCode",
|
||||
// Features that can be globally enabled or disabled
|
||||
"features": {
|
||||
// Show Copilot icon in status bar
|
||||
"copilot": true
|
||||
},
|
||||
// The name of a font to use for rendering text in the editor
|
||||
"buffer_font_family": "Zed Mono",
|
||||
// The OpenType features to enable for text in the editor.
|
||||
"buffer_font_features": {
|
||||
// Disable ligatures:
|
||||
// "calt": false
|
||||
},
|
||||
// The default font size for text in the editor
|
||||
"buffer_font_size": 15,
|
||||
// The factor to grow the active pane by. Defaults to 1.0
|
||||
// which gives the same size as all other panes.
|
||||
"active_pane_magnification": 1.0,
|
||||
// Whether to enable vim modes and key bindings
|
||||
"vim_mode": false,
|
||||
// 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 to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
// Controls whether copilot provides suggestion immediately
|
||||
// or waits for a `copilot::Toggle`
|
||||
"show_copilot_suggestions": true,
|
||||
// Whether to show tabs and spaces in the editor.
|
||||
// This setting can take two values:
|
||||
//
|
||||
// 1. Draw tabs and spaces only for the selected text (default):
|
||||
// "selection"
|
||||
// 2. Do not draw any tabs or spaces:
|
||||
// "none"
|
||||
// 3. Draw all invisible symbols:
|
||||
// "all"
|
||||
"show_whitespaces": "selection",
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the editor.
|
||||
// This setting can take four values:
|
||||
//
|
||||
// 1. Show the scrollbar if there's important information or
|
||||
// follow the system's configured behavior (default):
|
||||
// "auto"
|
||||
// 2. Match the system's configured behavior:
|
||||
// "system"
|
||||
// 3. Always show the scrollbar:
|
||||
// "always"
|
||||
// 4. Never show the scrollbar:
|
||||
// "never"
|
||||
"show": "auto",
|
||||
// Whether to show git diff indicators in the scrollbar.
|
||||
"git_diff": true
|
||||
},
|
||||
"project_panel": {
|
||||
// Whether to show the git status in the project panel.
|
||||
"git_status": true,
|
||||
// Where to dock project panel. Can be 'left' or 'right'.
|
||||
"dock": "left",
|
||||
// Default width of the project panel.
|
||||
"default_width": 240
|
||||
},
|
||||
// Whether the screen sharing icon is shown 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
|
||||
// take four values.
|
||||
//
|
||||
// 1. Never automatically save:
|
||||
// "autosave": "off",
|
||||
// 2. Save when changing focus away from the Zed window:
|
||||
// "autosave": "on_window_change",
|
||||
// 3. Save when changing focus away from a specific buffer:
|
||||
// "autosave": "on_focus_change",
|
||||
// 4. Save when idle for a certain amount of time:
|
||||
// "autosave": { "after_delay": {"milliseconds": 500} },
|
||||
"autosave": "off",
|
||||
// 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:
|
||||
//
|
||||
// 1. Format code using the current language server:
|
||||
// "format_on_save": "language_server"
|
||||
// 2. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// }
|
||||
"formatter": "language_server",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
//
|
||||
// 1. Do not soft wrap.
|
||||
// "soft_wrap": "none",
|
||||
// 2. Soft wrap lines that overflow the editor:
|
||||
// "soft_wrap": "editor_width",
|
||||
// 3. Soft wrap lines at the preferred line length
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "none",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
"preferred_line_length": 80,
|
||||
// Whether to indent lines using tab characters, as opposed to multiple
|
||||
// spaces.
|
||||
"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:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files"
|
||||
},
|
||||
"copilot": {
|
||||
// The set of glob patterns for which copilot should be disabled
|
||||
// in any matching file.
|
||||
"disabled_globs": [
|
||||
".env"
|
||||
]
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
// The path of the directory where journal entries are stored
|
||||
"path": "~",
|
||||
// What format to display the hours in
|
||||
// May take 2 values:
|
||||
// 1. hour12
|
||||
// 2. hour24
|
||||
"hour_format": "hour12"
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
// What shell to use when opening a terminal. May take 3 values:
|
||||
// 1. Use the system's default terminal configuration in /etc/passwd
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"formatter": "language_server",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
"shell": "system",
|
||||
// Where to dock terminals panel. Can be 'left', 'right', 'bottom'.
|
||||
"dock": "bottom",
|
||||
// Default width when the terminal is docked to the left or right.
|
||||
"default_width": 640,
|
||||
// Default height when the terminal is docked to the bottom.
|
||||
"default_height": 320,
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Will Fallback to the
|
||||
// first project directory strategy if unsuccessful
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
// "working_directory": "first_project_directory"
|
||||
// 3. Always use this platform's home directory (if we can find it)
|
||||
// "working_directory": "always_home"
|
||||
// 4. Always use a specific directory. This value will be shell expanded.
|
||||
// If this path is not a valid directory the terminal will default to
|
||||
// this platform's home directory (if we can find it)
|
||||
// "working_directory": {
|
||||
// "always": {
|
||||
// "directory": "~/zed/projects/"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// 1. Do not soft wrap.
|
||||
// "soft_wrap": "none",
|
||||
// 2. Soft wrap lines that overflow the editor:
|
||||
// "soft_wrap": "editor_width",
|
||||
// 3. Soft wrap lines at the preferred line length
|
||||
// "soft_wrap": "preferred_line_length",
|
||||
"soft_wrap": "none",
|
||||
// The column at which to soft-wrap lines, for buffers where soft-wrap
|
||||
// is enabled.
|
||||
"preferred_line_length": 80,
|
||||
// Whether to indent lines using tab characters, as opposed to multiple
|
||||
// spaces.
|
||||
"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
|
||||
//
|
||||
"working_directory": "current_project_directory",
|
||||
// Set the cursor blinking behavior in the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Never blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "off",
|
||||
// 2. Default the cursor blink to off, but allow the terminal to
|
||||
// set blinking
|
||||
// "blinking": "terminal_controlled",
|
||||
// 3. Always blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "on",
|
||||
"blinking": "terminal_controlled",
|
||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||
// presses when in the alternate screen (e.g. when running applications
|
||||
// like vim or less). The terminal can still set and unset this mode.
|
||||
// May take 2 values:
|
||||
// 1. Default alternate scroll mode to on
|
||||
// "alternate_scroll": "on",
|
||||
// 2. Default alternate scroll mode to off
|
||||
// "alternate_scroll": "off",
|
||||
"alternate_scroll": "off",
|
||||
// Set whether the option key behaves as the meta key.
|
||||
// May take 2 values:
|
||||
// 1. Rely on default platform handling of option key, on macOS
|
||||
// this means generating certain unicode characters
|
||||
// "option_to_meta": false,
|
||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||
// "option_to_meta": true,
|
||||
"option_as_meta": false,
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// enviroment. Use `:` to seperate multiple values.
|
||||
"env": {
|
||||
// "KEY": "value1:value2"
|
||||
},
|
||||
// Automatically update Zed
|
||||
"auto_update": true,
|
||||
// Git gutter behavior configuration.
|
||||
"git": {
|
||||
// Control whether the git gutter is shown. May take 2 values:
|
||||
// 1. Show the gutter
|
||||
// "git_gutter": "tracked_files"
|
||||
// 2. Hide the gutter
|
||||
// "git_gutter": "hide"
|
||||
"git_gutter": "tracked_files"
|
||||
// Set the terminal's line height.
|
||||
// May take 3 values:
|
||||
// 1. Use a line height that's comfortable for reading, 1.618
|
||||
// "line_height": "comfortable"
|
||||
// 2. Use a standard line height, 1.3. This option is useful for TUIs,
|
||||
// particularly if they use box characters
|
||||
// "line_height": "standard",
|
||||
// 3. Use a custom line height.
|
||||
// "line_height": {
|
||||
// "custom": 2
|
||||
// },
|
||||
//
|
||||
"line_height": "comfortable"
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
// "font_size": "15"
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Mono"
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
// Settings specific to journaling
|
||||
"journal": {
|
||||
// The path of the directory where journal entries are stored
|
||||
"path": "~",
|
||||
// What format to display the hours in
|
||||
// May take 2 values:
|
||||
// 1. hour12
|
||||
// 2. hour24
|
||||
"hour_format": "hour12"
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
// Settings specific to the terminal
|
||||
"terminal": {
|
||||
// What shell to use when opening a terminal. May take 3 values:
|
||||
// 1. Use the system's default terminal configuration in /etc/passwd
|
||||
// "shell": "system"
|
||||
// 2. A program:
|
||||
// "shell": {
|
||||
// "program": "sh"
|
||||
// }
|
||||
// 3. A program with arguments:
|
||||
// "shell": {
|
||||
// "with_arguments": {
|
||||
// "program": "/bin/bash",
|
||||
// "arguments": ["--login"]
|
||||
// }
|
||||
// }
|
||||
"shell": "system",
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Will Fallback to the
|
||||
// first project directory strategy if unsuccessful
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
// "working_directory": "first_project_directory"
|
||||
// 3. Always use this platform's home directory (if we can find it)
|
||||
// "working_directory": "always_home"
|
||||
// 4. Always use a specific directory. This value will be shell expanded.
|
||||
// If this path is not a valid directory the terminal will default to
|
||||
// this platform's home directory (if we can find it)
|
||||
// "working_directory": {
|
||||
// "always": {
|
||||
// "directory": "~/zed/projects/"
|
||||
// }
|
||||
// }
|
||||
//
|
||||
//
|
||||
"working_directory": "current_project_directory",
|
||||
// Set the cursor blinking behavior in the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Never blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "off",
|
||||
// 2. Default the cursor blink to off, but allow the terminal to
|
||||
// set blinking
|
||||
// "blinking": "terminal_controlled",
|
||||
// 3. Always blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "on",
|
||||
"blinking": "terminal_controlled",
|
||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||
// presses when in the alternate screen (e.g. when running applications
|
||||
// like vim or less). The terminal can still set and unset this mode.
|
||||
// May take 2 values:
|
||||
// 1. Default alternate scroll mode to on
|
||||
// "alternate_scroll": "on",
|
||||
// 2. Default alternate scroll mode to off
|
||||
// "alternate_scroll": "off",
|
||||
"alternate_scroll": "off",
|
||||
// Set whether the option key behaves as the meta key.
|
||||
// May take 2 values:
|
||||
// 1. Rely on default platform handling of option key, on macOS
|
||||
// this means generating certain unicode characters
|
||||
// "option_to_meta": false,
|
||||
// 2. Make the option keys behave as a 'meta' key, e.g. for emacs
|
||||
// "option_to_meta": true,
|
||||
"option_as_meta": false,
|
||||
// Whether or not selecting text in the terminal will automatically
|
||||
// copy to the system clipboard.
|
||||
"copy_on_select": false,
|
||||
// Any key-value pairs added to this list will be added to the terminal's
|
||||
// enviroment. Use `:` to seperate multiple values.
|
||||
"env": {
|
||||
// "KEY": "value1:value2"
|
||||
}
|
||||
// Set the terminal's font size. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font size.
|
||||
// "font_size": "15"
|
||||
// Set the terminal's font family. If this option is not included,
|
||||
// the terminal will default to matching the buffer's font family.
|
||||
// "font_family": "Zed Mono"
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true
|
||||
},
|
||||
// Different settings for specific languages.
|
||||
"languages": {
|
||||
"Plain Text": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"Elixir": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"Go": {
|
||||
"tab_size": 4,
|
||||
"hard_tabs": true
|
||||
},
|
||||
"Markdown": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"YAML": {
|
||||
"tab_size": 2
|
||||
}
|
||||
"Markdown": {
|
||||
"soft_wrap": "preferred_line_length"
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
// As of 8/10/22, supported LSPs are:
|
||||
// pyright
|
||||
// gopls
|
||||
// rust-analyzer
|
||||
// typescript-language-server
|
||||
// vscode-json-languageserver
|
||||
// "rust-analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
// "command": "clippy"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
"JavaScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TypeScript": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"TSX": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"YAML": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"JSON": {
|
||||
"tab_size": 2
|
||||
}
|
||||
},
|
||||
// LSP Specific settings.
|
||||
"lsp": {
|
||||
// Specify the LSP name as a key here.
|
||||
// As of 8/10/22, supported LSPs are:
|
||||
// pyright
|
||||
// gopls
|
||||
// rust-analyzer
|
||||
// typescript-language-server
|
||||
// vscode-json-languageserver
|
||||
// "rust-analyzer": {
|
||||
// //These initialization options are merged into Zed's defaults
|
||||
// "initialization_options": {
|
||||
// "checkOnSave": {
|
||||
// "command": "clippy"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
// custom settings, run the `open default settings` command
|
||||
// from the command palette or from `Zed` application menu.
|
||||
{
|
||||
"buffer_font_size": 15
|
||||
"buffer_font_size": 15
|
||||
}
|
||||
|
||||
@@ -16,6 +16,11 @@ gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
util = { path = "../util" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
futures = "0.3"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
|
||||
futures.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
||||
@@ -2,14 +2,13 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
|
||||
use editor::Editor;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
actions, anyhow,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, AppContext, Entity, ModelHandle, RenderContext, View, ViewContext, ViewHandle,
|
||||
AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use project::{LanguageServerProgress, Project};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc};
|
||||
use util::ResultExt;
|
||||
@@ -45,7 +44,7 @@ struct PendingWork<'a> {
|
||||
struct Content {
|
||||
icon: Option<&'static str>,
|
||||
message: String,
|
||||
action: Option<Box<dyn Action>>,
|
||||
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
@@ -63,21 +62,18 @@ impl ActivityIndicator {
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let this = cx.add_view(|cx: &mut ViewContext<Self>| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((language, event)) = status_events.next().await {
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != language.name());
|
||||
this.statuses.push(LspStatus {
|
||||
name: language.name(),
|
||||
status: event,
|
||||
});
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != language.name());
|
||||
this.statuses.push(LspStatus {
|
||||
name: language.name(),
|
||||
status: event,
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
cx.observe(&project, |_, _, cx| cx.notify()).detach();
|
||||
@@ -172,7 +168,7 @@ impl ActivityIndicator {
|
||||
.flatten()
|
||||
}
|
||||
|
||||
fn content_to_render(&mut self, cx: &mut RenderContext<Self>) -> Content {
|
||||
fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
|
||||
// Show any language server has pending activity.
|
||||
let mut pending_work = self.pending_language_server_work(cx);
|
||||
if let Some(PendingWork {
|
||||
@@ -202,7 +198,7 @@ impl ActivityIndicator {
|
||||
return Content {
|
||||
icon: None,
|
||||
message,
|
||||
action: None,
|
||||
on_click: None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -233,7 +229,7 @@ impl ActivityIndicator {
|
||||
downloading.join(", "),
|
||||
if downloading.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
action: None,
|
||||
on_click: None,
|
||||
};
|
||||
} else if !checking_for_update.is_empty() {
|
||||
return Content {
|
||||
@@ -247,7 +243,7 @@ impl ActivityIndicator {
|
||||
""
|
||||
}
|
||||
),
|
||||
action: None,
|
||||
on_click: None,
|
||||
};
|
||||
} else if !failed.is_empty() {
|
||||
return Content {
|
||||
@@ -257,7 +253,9 @@ impl ActivityIndicator {
|
||||
failed.join(", "),
|
||||
if failed.len() > 1 { "s" } else { "" }
|
||||
),
|
||||
action: Some(Box::new(ShowErrorMessage)),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.show_error_message(&Default::default(), cx)
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -267,27 +265,31 @@ impl ActivityIndicator {
|
||||
AutoUpdateStatus::Checking => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Checking for Zed updates…".to_string(),
|
||||
action: None,
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Downloading => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Downloading Zed update…".to_string(),
|
||||
action: None,
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Installing => Content {
|
||||
icon: Some(DOWNLOAD_ICON),
|
||||
message: "Installing Zed update…".to_string(),
|
||||
action: None,
|
||||
on_click: None,
|
||||
},
|
||||
AutoUpdateStatus::Updated => Content {
|
||||
icon: None,
|
||||
message: "Click to restart and update Zed".to_string(),
|
||||
action: Some(Box::new(workspace::Restart)),
|
||||
on_click: Some(Arc::new(|_, cx| {
|
||||
workspace::restart(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Errored => Content {
|
||||
icon: Some(WARNING_ICON),
|
||||
message: "Auto update failed".to_string(),
|
||||
action: Some(Box::new(DismissErrorMessage)),
|
||||
on_click: Some(Arc::new(|this, cx| {
|
||||
this.dismiss_error_message(&Default::default(), cx)
|
||||
})),
|
||||
},
|
||||
AutoUpdateStatus::Idle => Default::default(),
|
||||
};
|
||||
@@ -297,7 +299,7 @@ impl ActivityIndicator {
|
||||
return Content {
|
||||
icon: None,
|
||||
message: most_recent_active_task.to_string(),
|
||||
action: None,
|
||||
on_click: None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -314,21 +316,16 @@ impl View for ActivityIndicator {
|
||||
"ActivityIndicator"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let Content {
|
||||
icon,
|
||||
message,
|
||||
action,
|
||||
on_click,
|
||||
} = self.content_to_render(cx);
|
||||
|
||||
let mut element = MouseEventHandler::<Self>::new(0, cx, |state, cx| {
|
||||
let theme = &cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.lsp_status;
|
||||
let style = if state.hovered() && action.is_some() {
|
||||
let mut element = MouseEventHandler::<Self, _>::new(0, cx, |state, cx| {
|
||||
let theme = &theme::current(cx).workspace.status_bar.lsp_status;
|
||||
let style = if state.hovered() && on_click.is_some() {
|
||||
theme.hover.as_ref().unwrap_or(&theme.default)
|
||||
} else {
|
||||
&theme.default
|
||||
@@ -342,31 +339,27 @@ impl View for ActivityIndicator {
|
||||
.contained()
|
||||
.with_margin_right(style.icon_spacing)
|
||||
.aligned()
|
||||
.named("activity-icon")
|
||||
.into_any_named("activity-icon")
|
||||
}))
|
||||
.with_child(
|
||||
Text::new(message, style.message.clone())
|
||||
.with_soft_wrap(false)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(style.height)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.aligned()
|
||||
.boxed()
|
||||
});
|
||||
|
||||
if let Some(action) = action {
|
||||
if let Some(on_click) = on_click.clone() {
|
||||
element = element
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(action.boxed_clone())
|
||||
});
|
||||
.on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
|
||||
}
|
||||
|
||||
element.boxed()
|
||||
element.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
25
crates/ai/Cargo.toml
Normal file
25
crates/ai/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "ai"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/ai.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets"}
|
||||
collections = { path = "../collections"}
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
isahc.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
63
crates/ai/README.zmd
Normal file
63
crates/ai/README.zmd
Normal file
@@ -0,0 +1,63 @@
|
||||
> Hello, I'm Nathan Sobo.
|
||||
I'm currently your sole creator and the leader of Zed Industries.
|
||||
Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation.
|
||||
|
||||
< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development.
|
||||
|
||||
As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world.
|
||||
|
||||
> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish.
|
||||
|
||||
We've decided that our next milestone will be open-sourcing Zed on the Zed platform.
|
||||
|
||||
< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further.
|
||||
|
||||
Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation.
|
||||
|
||||
Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future!
|
||||
|
||||
@nathan> #zed, can you describe the conversation UI as you currently understand it.
|
||||
|
||||
< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary.
|
||||
|
||||
Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered.
|
||||
|
||||
@nathan> What details around the conversation view are worth exploring?
|
||||
|
||||
< @nathan, there are several aspects of the conversation view that are worth exploring and refining:
|
||||
|
||||
1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context.
|
||||
2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history.
|
||||
3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials.
|
||||
4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants.
|
||||
5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view.
|
||||
|
||||
Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed.
|
||||
|
||||
@nathan> @nate, which of these would you like to explore?
|
||||
|
||||
@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5.
|
||||
|
||||
#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots).
|
||||
|
||||
@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try...
|
||||
|
||||
#zed describe a hypothetical conversation UI that meets the description.
|
||||
|
||||
< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements:
|
||||
|
||||
1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space.
|
||||
|
||||
2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions.
|
||||
|
||||
3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view.
|
||||
|
||||
4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly.
|
||||
|
||||
5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow.
|
||||
|
||||
6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view.
|
||||
|
||||
7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context.
|
||||
|
||||
This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs.
|
||||
3
crates/ai/features.zmd
Normal file
3
crates/ai/features.zmd
Normal file
@@ -0,0 +1,3 @@
|
||||
Push content to a deeper layer.
|
||||
A context can have multiple sublayers.
|
||||
You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document.
|
||||
313
crates/ai/src/ai.rs
Normal file
313
crates/ai/src/ai.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assets::Assets;
|
||||
use collections::HashMap;
|
||||
use editor::Editor;
|
||||
use futures::AsyncBufReadExt;
|
||||
use futures::{io::BufReader, AsyncReadExt, Stream, StreamExt};
|
||||
use gpui::executor::Background;
|
||||
use gpui::{actions, AppContext, Task, ViewContext};
|
||||
use isahc::prelude::*;
|
||||
use isahc::{http::StatusCode, Request};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cell::RefCell;
|
||||
use std::fs;
|
||||
use std::rc::Rc;
|
||||
use std::{io, sync::Arc};
|
||||
use util::channel::{ReleaseChannel, RELEASE_CHANNEL};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
actions!(ai, [Assist]);
|
||||
|
||||
// Data types for chat completion requests
|
||||
#[derive(Serialize)]
|
||||
struct OpenAIRequest {
|
||||
model: String,
|
||||
messages: Vec<RequestMessage>,
|
||||
stream: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct RequestMessage {
|
||||
role: Role,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
struct ResponseMessage {
|
||||
role: Option<Role>,
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum Role {
|
||||
User,
|
||||
Assistant,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenAIResponseStreamEvent {
|
||||
pub id: Option<String>,
|
||||
pub object: String,
|
||||
pub created: u32,
|
||||
pub model: String,
|
||||
pub choices: Vec<ChatChoiceDelta>,
|
||||
pub usage: Option<Usage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Usage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct ChatChoiceDelta {
|
||||
pub index: u32,
|
||||
pub delta: ResponseMessage,
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenAIUsage {
|
||||
prompt_tokens: u64,
|
||||
completion_tokens: u64,
|
||||
total_tokens: u64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct OpenAIChoice {
|
||||
text: String,
|
||||
index: u32,
|
||||
logprobs: Option<serde_json::Value>,
|
||||
finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
if *RELEASE_CHANNEL == ReleaseChannel::Stable {
|
||||
return;
|
||||
}
|
||||
|
||||
let assistant = Rc::new(Assistant::default());
|
||||
cx.add_action({
|
||||
let assistant = assistant.clone();
|
||||
move |editor: &mut Editor, _: &Assist, cx: &mut ViewContext<Editor>| {
|
||||
assistant.assist(editor, cx).log_err();
|
||||
}
|
||||
});
|
||||
cx.capture_action({
|
||||
let assistant = assistant.clone();
|
||||
move |_: &mut Editor, _: &editor::Cancel, cx: &mut ViewContext<Editor>| {
|
||||
if !assistant.cancel_last_assist(cx.view_id()) {
|
||||
cx.propagate_action();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type CompletionId = usize;
|
||||
|
||||
#[derive(Default)]
|
||||
struct Assistant(RefCell<AssistantState>);
|
||||
|
||||
#[derive(Default)]
|
||||
struct AssistantState {
|
||||
assist_stacks: HashMap<usize, Vec<(CompletionId, Task<Option<()>>)>>,
|
||||
next_completion_id: CompletionId,
|
||||
}
|
||||
|
||||
impl Assistant {
|
||||
fn assist(self: &Rc<Self>, editor: &mut Editor, cx: &mut ViewContext<Editor>) -> Result<()> {
|
||||
let api_key = std::env::var("OPENAI_API_KEY")?;
|
||||
|
||||
let selections = editor.selections.all(cx);
|
||||
let (user_message, insertion_site) = editor.buffer().update(cx, |buffer, cx| {
|
||||
// Insert markers around selected text as described in the system prompt above.
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let mut user_message = String::new();
|
||||
let mut user_message_suffix = String::new();
|
||||
let mut buffer_offset = 0;
|
||||
for selection in selections {
|
||||
if !selection.is_empty() {
|
||||
if user_message_suffix.is_empty() {
|
||||
user_message_suffix.push_str("\n\n");
|
||||
}
|
||||
user_message_suffix.push_str("[Selected excerpt from above]\n");
|
||||
user_message_suffix
|
||||
.extend(snapshot.text_for_range(selection.start..selection.end));
|
||||
user_message_suffix.push_str("\n\n");
|
||||
}
|
||||
|
||||
user_message.extend(snapshot.text_for_range(buffer_offset..selection.start));
|
||||
user_message.push_str("[SELECTION_START]");
|
||||
user_message.extend(snapshot.text_for_range(selection.start..selection.end));
|
||||
buffer_offset = selection.end;
|
||||
user_message.push_str("[SELECTION_END]");
|
||||
}
|
||||
if buffer_offset < snapshot.len() {
|
||||
user_message.extend(snapshot.text_for_range(buffer_offset..snapshot.len()));
|
||||
}
|
||||
user_message.push_str(&user_message_suffix);
|
||||
|
||||
// Ensure the document ends with 4 trailing newlines.
|
||||
let trailing_newline_count = snapshot
|
||||
.reversed_chars_at(snapshot.len())
|
||||
.take_while(|c| *c == '\n')
|
||||
.take(4);
|
||||
let buffer_suffix = "\n".repeat(4 - trailing_newline_count.count());
|
||||
buffer.edit([(snapshot.len()..snapshot.len(), buffer_suffix)], None, cx);
|
||||
|
||||
let snapshot = buffer.snapshot(cx); // Take a new snapshot after editing.
|
||||
let insertion_site = snapshot.anchor_after(snapshot.len() - 2);
|
||||
|
||||
(user_message, insertion_site)
|
||||
});
|
||||
|
||||
let this = self.clone();
|
||||
let buffer = editor.buffer().clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
let editor_id = cx.view_id();
|
||||
let assist_id = util::post_inc(&mut self.0.borrow_mut().next_completion_id);
|
||||
let assist_task = cx.spawn(|_, mut cx| {
|
||||
async move {
|
||||
// TODO: We should have a get_string method on assets. This is repateated elsewhere.
|
||||
let content = Assets::get("contexts/system.zmd").unwrap();
|
||||
let mut system_message = std::str::from_utf8(content.data.as_ref())
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
if let Ok(custom_system_message_path) =
|
||||
std::env::var("ZED_ASSISTANT_SYSTEM_PROMPT_PATH")
|
||||
{
|
||||
system_message.push_str(
|
||||
"\n\nAlso consider the following user-defined system prompt:\n\n",
|
||||
);
|
||||
// TODO: Replace this with our file system trait object.
|
||||
system_message.push_str(
|
||||
&cx.background()
|
||||
.spawn(async move { fs::read_to_string(custom_system_message_path) })
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
let stream = stream_completion(
|
||||
api_key,
|
||||
executor,
|
||||
OpenAIRequest {
|
||||
model: "gpt-4".to_string(),
|
||||
messages: vec![
|
||||
RequestMessage {
|
||||
role: Role::System,
|
||||
content: system_message.to_string(),
|
||||
},
|
||||
RequestMessage {
|
||||
role: Role::User,
|
||||
content: user_message,
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
},
|
||||
);
|
||||
|
||||
let mut messages = stream.await?;
|
||||
while let Some(message) = messages.next().await {
|
||||
let mut message = message?;
|
||||
if let Some(choice) = message.choices.pop() {
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
let text: Arc<str> = choice.delta.content?.into();
|
||||
buffer.edit([(insertion_site.clone()..insertion_site, text)], None, cx);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.get_mut(&editor_id)
|
||||
.unwrap()
|
||||
.retain(|(id, _)| *id != assist_id);
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.entry(cx.view_id())
|
||||
.or_default()
|
||||
.push((assist_id, assist_task));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cancel_last_assist(self: &Rc<Self>, editor_id: usize) -> bool {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.assist_stacks
|
||||
.get_mut(&editor_id)
|
||||
.and_then(|assists| assists.pop())
|
||||
.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
async fn stream_completion(
|
||||
api_key: String,
|
||||
executor: Arc<Background>,
|
||||
mut request: OpenAIRequest,
|
||||
) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
|
||||
request.stream = true;
|
||||
|
||||
let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
|
||||
|
||||
let json_data = serde_json::to_string(&request)?;
|
||||
let mut response = Request::post("https://api.openai.com/v1/chat/completions")
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.body(json_data)?
|
||||
.send_async()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if status == StatusCode::OK {
|
||||
executor
|
||||
.spawn(async move {
|
||||
let mut lines = BufReader::new(response.body_mut()).lines();
|
||||
|
||||
fn parse_line(
|
||||
line: Result<String, io::Error>,
|
||||
) -> Result<Option<OpenAIResponseStreamEvent>> {
|
||||
if let Some(data) = line?.strip_prefix("data: ") {
|
||||
let event = serde_json::from_str(&data)?;
|
||||
Ok(Some(event))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(line) = lines.next().await {
|
||||
if let Some(event) = parse_line(line).transpose() {
|
||||
tx.unbounded_send(event).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok(rx)
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
Err(anyhow!(
|
||||
"Failed to connect to OpenAI API: {} {}",
|
||||
response.status(),
|
||||
body,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,5 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
gpui = { path = "../gpui" }
|
||||
anyhow = "1.0.38"
|
||||
anyhow.workspace = true
|
||||
rust-embed = { version = "6.3", features = ["include-exclude"] }
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace" }
|
||||
util = { path = "../util" }
|
||||
anyhow = "1.0.38"
|
||||
isahc = "1.7"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
smol = "1.2.5"
|
||||
tempdir = "0.3.7"
|
||||
anyhow.workspace = true
|
||||
isahc.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tempdir.workspace = true
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
mod update_notification;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use client::{ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
|
||||
use client::{Client, TelemetrySettings, ZED_APP_PATH, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
Task, WeakViewHandle,
|
||||
};
|
||||
use isahc::AsyncBody;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use serde_derive::Serialize;
|
||||
use settings::{Setting, SettingsStore};
|
||||
use smol::{fs::File, io::AsyncReadExt, process::Command};
|
||||
use std::{ffi::OsString, sync::Arc, time::Duration};
|
||||
use update_notification::UpdateNotification;
|
||||
@@ -21,6 +23,13 @@ const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes]);
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct UpdateRequestBody {
|
||||
installation_id: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
telemetry: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AutoUpdateStatus {
|
||||
Idle,
|
||||
@@ -49,19 +58,37 @@ impl Entity for AutoUpdater {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
|
||||
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
|
||||
let server_url = server_url;
|
||||
let auto_updater = cx.add_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url.clone());
|
||||
struct AutoUpdateSetting(bool);
|
||||
|
||||
let mut update_subscription = cx
|
||||
.global::<Settings>()
|
||||
.auto_update
|
||||
impl Setting for AutoUpdateSetting {
|
||||
const KEY: Option<&'static str> = Some("auto_update");
|
||||
|
||||
type FileContent = Option<bool>;
|
||||
|
||||
fn load(
|
||||
default_value: &Option<bool>,
|
||||
user_values: &[&Option<bool>],
|
||||
_: &AppContext,
|
||||
) -> Result<Self> {
|
||||
Ok(Self(
|
||||
Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
|
||||
settings::register::<AutoUpdateSetting>(cx);
|
||||
|
||||
if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) {
|
||||
let auto_updater = cx.add_model(|cx| {
|
||||
let updater = AutoUpdater::new(version, http_client, server_url);
|
||||
|
||||
let mut update_subscription = settings::get::<AutoUpdateSetting>(cx)
|
||||
.0
|
||||
.then(|| updater.start_polling(cx));
|
||||
|
||||
cx.observe_global::<Settings, _>(move |updater, cx| {
|
||||
if cx.global::<Settings>().auto_update {
|
||||
cx.observe_global::<SettingsStore, _>(move |updater, cx| {
|
||||
if settings::get::<AutoUpdateSetting>(cx).0 {
|
||||
if update_subscription.is_none() {
|
||||
update_subscription = Some(updater.start_polling(cx))
|
||||
}
|
||||
@@ -74,25 +101,32 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
|
||||
updater
|
||||
});
|
||||
cx.set_global(Some(auto_updater));
|
||||
cx.add_global_action(|_: &Check, cx| {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
}
|
||||
});
|
||||
cx.add_global_action(move |_: &ViewReleaseNotes, cx| {
|
||||
let latest_release_url = if cx.has_global::<ReleaseChannel>()
|
||||
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
{
|
||||
format!("{server_url}/releases/preview/latest")
|
||||
} else {
|
||||
format!("{server_url}/releases/latest")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
});
|
||||
cx.add_global_action(check);
|
||||
cx.add_global_action(view_release_notes);
|
||||
cx.add_action(UpdateNotification::dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(_: &Check, cx: &mut AppContext) {
|
||||
if let Some(updater) = AutoUpdater::get(cx) {
|
||||
updater.update(cx, |updater, cx| updater.poll(cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
|
||||
if let Some(auto_updater) = AutoUpdater::get(cx) {
|
||||
let server_url = &auto_updater.read(cx).server_url;
|
||||
let latest_release_url = if cx.has_global::<ReleaseChannel>()
|
||||
&& *cx.global::<ReleaseChannel>() == ReleaseChannel::Preview
|
||||
{
|
||||
format!("{server_url}/releases/preview/latest")
|
||||
} else {
|
||||
format!("{server_url}/releases/stable/latest")
|
||||
};
|
||||
cx.platform().open_url(&latest_release_url);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notify_of_any_new_update(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut AppContext,
|
||||
@@ -104,17 +138,15 @@ pub fn notify_of_any_new_update(
|
||||
cx.spawn(|mut cx| async move {
|
||||
let should_show_notification = should_show_notification.await?;
|
||||
if should_show_notification {
|
||||
if let Some(workspace) = workspace.upgrade(&cx) {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.show_notification(0, cx, |cx| {
|
||||
cx.add_view(|_| UpdateNotification::new(version))
|
||||
});
|
||||
updater
|
||||
.read(cx)
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.show_notification(0, cx, |cx| {
|
||||
cx.add_view(|_| UpdateNotification::new(version))
|
||||
});
|
||||
}
|
||||
updater
|
||||
.read(cx)
|
||||
.set_should_show_update_notification(false, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
@@ -243,7 +275,24 @@ impl AutoUpdater {
|
||||
mounted_app_path.push("/");
|
||||
|
||||
let mut dmg_file = File::create(&dmg_path).await?;
|
||||
let mut response = client.get(&release.url, Default::default(), true).await?;
|
||||
|
||||
let (installation_id, release_channel, telemetry) = cx.read(|cx| {
|
||||
let installation_id = cx.global::<Arc<Client>>().telemetry().installation_id();
|
||||
let release_channel = cx
|
||||
.has_global::<ReleaseChannel>()
|
||||
.then(|| cx.global::<ReleaseChannel>().display_name());
|
||||
let telemetry = settings::get::<TelemetrySettings>(cx).metrics;
|
||||
|
||||
(installation_id, release_channel, telemetry)
|
||||
});
|
||||
|
||||
let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
|
||||
installation_id,
|
||||
release_channel,
|
||||
telemetry,
|
||||
})?);
|
||||
|
||||
let mut response = client.get(&release.url, request_body, true).await?;
|
||||
smol::io::copy(response.body_mut(), &mut dmg_file).await?;
|
||||
log::info!("downloaded update. path:{:?}", dmg_path);
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use gpui::{
|
||||
Element, Entity, View, ViewContext,
|
||||
};
|
||||
use menu::Cancel;
|
||||
use settings::Settings;
|
||||
use util::channel::ReleaseChannel;
|
||||
use workspace::notifications::Notification;
|
||||
|
||||
@@ -26,13 +25,13 @@ impl View for UpdateNotification {
|
||||
"UpdateNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||
let theme = theme::current(cx).clone();
|
||||
let theme = &theme.update_notification;
|
||||
|
||||
let app_name = cx.global::<ReleaseChannel>().display_name();
|
||||
|
||||
MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
|
||||
MouseEventHandler::<ViewReleaseNotes, _>::new(0, cx, |state, cx| {
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
@@ -46,11 +45,10 @@ impl View for UpdateNotification {
|
||||
.aligned()
|
||||
.top()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
@@ -62,35 +60,32 @@ impl View for UpdateNotification {
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.boxed()
|
||||
})
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, cx| cx.dispatch_action(Cancel))
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.dismiss(&Default::default(), cx)
|
||||
})
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(cx.font_cache().line_height(theme.message.text.font_size))
|
||||
.aligned()
|
||||
.top()
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
.flex_float(),
|
||||
),
|
||||
)
|
||||
.with_child({
|
||||
let style = theme.action_message.style_for(state, false);
|
||||
Text::new("View the release notes", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.contained()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(ViewReleaseNotes)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
crate::view_release_notes(&Default::default(), cx)
|
||||
})
|
||||
.boxed()
|
||||
.into_any_named("update notification")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use gpui::{
|
||||
elements::*, platform::MouseButton, AppContext, Entity, RenderContext, Subscription, View,
|
||||
ViewContext, ViewHandle,
|
||||
elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use search::ProjectSearchView;
|
||||
use settings::Settings;
|
||||
use workspace::{
|
||||
item::{ItemEvent, ItemHandle},
|
||||
ToolbarItemLocation, ToolbarItemView,
|
||||
ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
pub enum Event {
|
||||
@@ -19,15 +18,17 @@ pub struct Breadcrumbs {
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
project_search: Option<ViewHandle<ProjectSearchView>>,
|
||||
subscription: Option<Subscription>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
}
|
||||
|
||||
impl Breadcrumbs {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(workspace: &Workspace) -> Self {
|
||||
Self {
|
||||
pane_focused: false,
|
||||
active_item: Default::default(),
|
||||
subscription: Default::default(),
|
||||
project_search: Default::default(),
|
||||
workspace: workspace.weak_handle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,24 +42,33 @@ impl View for Breadcrumbs {
|
||||
"Breadcrumbs"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let active_item = match &self.active_item {
|
||||
Some(active_item) => active_item,
|
||||
None => return Empty::new().boxed(),
|
||||
None => return Empty::new().into_any(),
|
||||
};
|
||||
let not_editor = active_item.downcast::<editor::Editor>().is_none();
|
||||
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = theme::current(cx).clone();
|
||||
let style = &theme.workspace.breadcrumbs;
|
||||
|
||||
let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
|
||||
Some(breadcrumbs) => breadcrumbs,
|
||||
None => return Empty::new().boxed(),
|
||||
};
|
||||
None => return Empty::new().into_any(),
|
||||
}
|
||||
.into_iter()
|
||||
.map(|breadcrumb| {
|
||||
Text::new(
|
||||
breadcrumb.text,
|
||||
theme.workspace.breadcrumbs.default.text.clone(),
|
||||
)
|
||||
.with_highlights(breadcrumb.highlights.unwrap_or_default())
|
||||
.into_any()
|
||||
});
|
||||
|
||||
let crumbs = Flex::row()
|
||||
.with_children(Itertools::intersperse_with(breadcrumbs.into_iter(), || {
|
||||
Label::new(" 〉 ", style.default.text.clone()).boxed()
|
||||
.with_children(Itertools::intersperse_with(breadcrumbs, || {
|
||||
Label::new(" 〉 ", style.default.text.clone()).into_any()
|
||||
}))
|
||||
.constrained()
|
||||
.with_height(theme.workspace.breadcrumb_height)
|
||||
@@ -69,17 +79,21 @@ impl View for Breadcrumbs {
|
||||
.with_style(style.default.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed();
|
||||
.into_any();
|
||||
}
|
||||
|
||||
MouseEventHandler::<Breadcrumbs>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<Breadcrumbs, Breadcrumbs>::new(0, cx, |state, _| {
|
||||
let style = style.style_for(state, false);
|
||||
crumbs.with_style(style.container).boxed()
|
||||
crumbs.with_style(style.container)
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(outline::Toggle);
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
outline::toggle(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Breadcrumbs, _>(
|
||||
.with_tooltip::<Breadcrumbs>(
|
||||
0,
|
||||
"Show symbol outline".to_owned(),
|
||||
Some(Box::new(outline::Toggle)),
|
||||
@@ -88,7 +102,7 @@ impl View for Breadcrumbs {
|
||||
)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +150,7 @@ impl ToolbarItemView for Breadcrumbs {
|
||||
}
|
||||
}
|
||||
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut gpui::AppContext) {
|
||||
fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
|
||||
self.pane_focused = pane_focused;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ test-support = [
|
||||
client = { path = "../client" }
|
||||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
log = "0.4"
|
||||
log.workspace = true
|
||||
live_kit_client = { path = "../live_kit_client" }
|
||||
fs = { path = "../fs" }
|
||||
language = { path = "../language" }
|
||||
@@ -31,10 +31,10 @@ project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
anyhow.workspace = true
|
||||
async-broadcast = "0.4"
|
||||
futures = "0.3"
|
||||
postage = { workspace = true }
|
||||
futures.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
|
||||
@@ -13,12 +13,13 @@ name = "cli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
anyhow.workspace = true
|
||||
clap = { version = "3.1", features = ["derive"] }
|
||||
dirs = "3.0"
|
||||
ipc-channel = "0.16"
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
util = { path = "../util" }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.9"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
pub use ipc_channel::ipc;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct IpcHandshake {
|
||||
@@ -10,7 +9,12 @@ pub struct IpcHandshake {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliRequest {
|
||||
Open { paths: Vec<PathBuf>, wait: bool },
|
||||
// The filed is named `path` for compatibility, but now CLI can request
|
||||
// opening a path at a certain row and/or column: `some/path:123` and `some/path:123:456`.
|
||||
//
|
||||
// Since Zed CLI has to be installed separately, there can be situations when old CLI is
|
||||
// querying new Zed editors, support both formats by using `String` here and parsing it on Zed side later.
|
||||
Open { paths: Vec<String>, wait: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -20,3 +24,7 @@ pub enum CliResponse {
|
||||
Stderr { message: String },
|
||||
Exit { status: i32 },
|
||||
}
|
||||
|
||||
/// When Zed started not as an *.app but as a binary (e.g. local development),
|
||||
/// there's a possibility to tell it to behave "regularly".
|
||||
pub const FORCE_CLI_MODE_ENV_VAR_NAME: &str = "ZED_FORCE_CLI_MODE";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::Parser;
|
||||
use cli::{CliRequest, CliResponse, IpcHandshake};
|
||||
use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
|
||||
use core_foundation::{
|
||||
array::{CFArray, CFIndex},
|
||||
string::kCFStringEncodingUTF8,
|
||||
@@ -16,16 +16,20 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
ptr,
|
||||
};
|
||||
use util::paths::PathLikeWithPosition;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
|
||||
struct Args {
|
||||
/// Wait for all of the given paths to be closed before exiting.
|
||||
/// Wait for all of the given paths to be opened/closed before exiting.
|
||||
#[clap(short, long)]
|
||||
wait: bool,
|
||||
/// A sequence of space-separated paths that you want to open.
|
||||
#[clap()]
|
||||
paths: Vec<PathBuf>,
|
||||
///
|
||||
/// Use `path:line:row` syntax to open a file at a specific location.
|
||||
/// Non-existing paths and directories will ignore `:line:row` suffix.
|
||||
#[clap(value_parser = parse_path_with_position)]
|
||||
paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
|
||||
/// Print Zed's version and the app path.
|
||||
#[clap(short, long)]
|
||||
version: bool,
|
||||
@@ -34,6 +38,14 @@ struct Args {
|
||||
bundle_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn parse_path_with_position(
|
||||
argument_str: &str,
|
||||
) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
|
||||
PathLikeWithPosition::parse_str(argument_str, |path_str| {
|
||||
Ok(Path::new(path_str).to_path_buf())
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct InfoPlist {
|
||||
#[serde(rename = "CFBundleShortVersionString")]
|
||||
@@ -43,37 +55,37 @@ struct InfoPlist {
|
||||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let bundle_path = if let Some(bundle_path) = args.bundle_path {
|
||||
bundle_path.canonicalize()?
|
||||
} else {
|
||||
locate_bundle()?
|
||||
};
|
||||
let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
|
||||
|
||||
if args.version {
|
||||
let plist_path = bundle_path.join("Contents/Info.plist");
|
||||
let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
|
||||
println!(
|
||||
"Zed {} – {}",
|
||||
plist.bundle_short_version_string,
|
||||
bundle_path.to_string_lossy()
|
||||
);
|
||||
println!("{}", bundle.zed_version_string());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for path in args.paths.iter() {
|
||||
for path in args
|
||||
.paths_with_position
|
||||
.iter()
|
||||
.map(|path_with_position| &path_with_position.path_like)
|
||||
{
|
||||
if !path.exists() {
|
||||
touch(path.as_path())?;
|
||||
}
|
||||
}
|
||||
|
||||
let (tx, rx) = launch_app(bundle_path)?;
|
||||
let (tx, rx) = bundle.launch()?;
|
||||
|
||||
tx.send(CliRequest::Open {
|
||||
paths: args
|
||||
.paths
|
||||
.paths_with_position
|
||||
.into_iter()
|
||||
.map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
|
||||
.collect::<Result<Vec<PathBuf>>>()?,
|
||||
.map(|path_with_position| {
|
||||
let path_with_position = path_with_position.map_path_like(|path| {
|
||||
fs::canonicalize(&path)
|
||||
.with_context(|| format!("path {path:?} canonicalization"))
|
||||
})?;
|
||||
Ok(path_with_position.to_string(|path| path.display().to_string()))
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
wait: args.wait,
|
||||
})?;
|
||||
|
||||
@@ -89,6 +101,148 @@ fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Bundle {
|
||||
App {
|
||||
app_bundle: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
LocalPath {
|
||||
executable: PathBuf,
|
||||
plist: InfoPlist,
|
||||
},
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
|
||||
let bundle_path = if let Some(bundle_path) = args_bundle_path {
|
||||
bundle_path
|
||||
.canonicalize()
|
||||
.with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
|
||||
} else {
|
||||
locate_bundle().context("bundle autodiscovery")?
|
||||
};
|
||||
|
||||
match bundle_path.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("app") => {
|
||||
let plist_path = bundle_path.join("Contents/Info.plist");
|
||||
let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
|
||||
format!("Reading *.app bundle plist file at {plist_path:?}")
|
||||
})?;
|
||||
Ok(Self::App {
|
||||
app_bundle: bundle_path,
|
||||
plist,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
|
||||
let plist_path = bundle_path
|
||||
.parent()
|
||||
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
|
||||
.join("WebRTC.framework/Resources/Info.plist");
|
||||
let plist = plist::from_file::<_, InfoPlist>(&plist_path)
|
||||
.with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
|
||||
Ok(Self::LocalPath {
|
||||
executable: bundle_path,
|
||||
plist,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plist(&self) -> &InfoPlist {
|
||||
match self {
|
||||
Self::App { plist, .. } => plist,
|
||||
Self::LocalPath { plist, .. } => plist,
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => app_bundle,
|
||||
Self::LocalPath {
|
||||
executable: excutable,
|
||||
..
|
||||
} => excutable,
|
||||
}
|
||||
}
|
||||
|
||||
fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
let (server, server_name) =
|
||||
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
match self {
|
||||
Self::App { app_bundle, .. } => {
|
||||
let app_path = app_bundle;
|
||||
|
||||
let status = unsafe {
|
||||
let app_url = CFURL::from_path(app_path, true)
|
||||
.with_context(|| format!("invalid app path {app_path:?}"))?;
|
||||
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
|
||||
ptr::null(),
|
||||
url.as_ptr(),
|
||||
url.len() as CFIndex,
|
||||
kCFStringEncodingUTF8,
|
||||
ptr::null(),
|
||||
));
|
||||
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
|
||||
LSOpenFromURLSpec(
|
||||
&LSLaunchURLSpec {
|
||||
appURL: app_url.as_concrete_TypeRef(),
|
||||
itemURLs: urls_to_open.as_concrete_TypeRef(),
|
||||
passThruParams: ptr::null(),
|
||||
launchFlags: kLSLaunchDefaults,
|
||||
asyncRefCon: ptr::null_mut(),
|
||||
},
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
anyhow::ensure!(
|
||||
status == 0,
|
||||
"cannot start app bundle {}",
|
||||
self.zed_version_string()
|
||||
);
|
||||
}
|
||||
Self::LocalPath { executable, .. } => {
|
||||
let executable_parent = executable
|
||||
.parent()
|
||||
.with_context(|| format!("Executable {executable:?} path has no parent"))?;
|
||||
let subprocess_stdout_file =
|
||||
fs::File::create(executable_parent.join("zed_dev.log"))
|
||||
.with_context(|| format!("Log file creation in {executable_parent:?}"))?;
|
||||
let subprocess_stdin_file =
|
||||
subprocess_stdout_file.try_clone().with_context(|| {
|
||||
format!("Cloning descriptor for file {subprocess_stdout_file:?}")
|
||||
})?;
|
||||
let mut command = std::process::Command::new(executable);
|
||||
let command = command
|
||||
.env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
|
||||
.stderr(subprocess_stdout_file)
|
||||
.stdout(subprocess_stdin_file)
|
||||
.arg(url);
|
||||
|
||||
command
|
||||
.spawn()
|
||||
.with_context(|| format!("Spawning {command:?}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
|
||||
Ok((handshake.requests, handshake.responses))
|
||||
}
|
||||
|
||||
fn zed_version_string(&self) -> String {
|
||||
let is_dev = matches!(self, Self::LocalPath { .. });
|
||||
format!(
|
||||
"Zed {}{} – {}",
|
||||
self.plist().bundle_short_version_string,
|
||||
if is_dev { " (dev)" } else { "" },
|
||||
self.path().display(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn touch(path: &Path) -> io::Result<()> {
|
||||
match OpenOptions::new().create(true).write(true).open(path) {
|
||||
Ok(_) => Ok(()),
|
||||
@@ -106,38 +260,3 @@ fn locate_bundle() -> Result<PathBuf> {
|
||||
}
|
||||
Ok(app_path)
|
||||
}
|
||||
|
||||
fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
|
||||
let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
|
||||
let url = format!("zed-cli://{server_name}");
|
||||
|
||||
let status = unsafe {
|
||||
let app_url =
|
||||
CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
|
||||
let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
|
||||
ptr::null(),
|
||||
url.as_ptr(),
|
||||
url.len() as CFIndex,
|
||||
kCFStringEncodingUTF8,
|
||||
ptr::null(),
|
||||
));
|
||||
let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
|
||||
LSOpenFromURLSpec(
|
||||
&LSLaunchURLSpec {
|
||||
appURL: app_url.as_concrete_TypeRef(),
|
||||
itemURLs: urls_to_open.as_concrete_TypeRef(),
|
||||
passThruParams: ptr::null(),
|
||||
launchFlags: kLSLaunchDefaults,
|
||||
asyncRefCon: ptr::null_mut(),
|
||||
},
|
||||
ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
if status == 0 {
|
||||
let (_, handshake) = server.accept()?;
|
||||
Ok((handshake.requests, handshake.responses))
|
||||
} else {
|
||||
Err(anyhow!("cannot start {:?}", app_path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,27 +17,29 @@ db = { path = "../db" }
|
||||
gpui = { path = "../gpui" }
|
||||
util = { path = "../util" }
|
||||
rpc = { path = "../rpc" }
|
||||
settings = { path = "../settings" }
|
||||
staff_mode = { path = "../staff_mode" }
|
||||
sum_tree = { path = "../sum_tree" }
|
||||
anyhow = "1.0.38"
|
||||
|
||||
anyhow.workspace = true
|
||||
async-recursion = "0.3"
|
||||
async-tungstenite = { version = "0.16", features = ["async-tls"] }
|
||||
futures = "0.3"
|
||||
futures.workspace = true
|
||||
image = "0.23"
|
||||
lazy_static = "1.4.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
parking_lot = "0.11.1"
|
||||
postage = { workspace = true }
|
||||
rand = "0.8.3"
|
||||
smol = "1.2.5"
|
||||
thiserror = "1.0.29"
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
schemars.workspace = true
|
||||
smol.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http = "0.8"
|
||||
uuid = { version = "1.1.2", features = ["v4"] }
|
||||
url = "2.2"
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
settings = { path = "../settings" }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
tempfile = "3"
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -15,19 +15,17 @@ use futures::{
|
||||
TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions,
|
||||
platform::AppVersion,
|
||||
serde_json::{self, Value},
|
||||
AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AppContext,
|
||||
AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext, ViewHandle,
|
||||
actions, platform::AppVersion, serde_json, AnyModelHandle, AnyWeakModelHandle,
|
||||
AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelHandle, Task, View, ViewContext,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
use rand::prelude::*;
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings, TelemetrySettings};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
collections::HashMap,
|
||||
@@ -47,6 +45,7 @@ use util::http::HttpClient;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub use rpc::*;
|
||||
pub use telemetry::ClickhouseEvent;
|
||||
pub use user::*;
|
||||
|
||||
lazy_static! {
|
||||
@@ -71,25 +70,34 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
actions!(client, [SignIn, SignOut]);
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
pub fn init_settings(cx: &mut AppContext) {
|
||||
settings::register::<TelemetrySettings>(cx);
|
||||
}
|
||||
|
||||
pub fn init(client: &Arc<Client>, cx: &mut AppContext) {
|
||||
init_settings(cx);
|
||||
|
||||
let client = Arc::downgrade(client);
|
||||
cx.add_global_action({
|
||||
let client = client.clone();
|
||||
move |_: &SignIn, cx| {
|
||||
let client = client.clone();
|
||||
cx.spawn(
|
||||
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
|
||||
)
|
||||
.detach();
|
||||
if let Some(client) = client.upgrade() {
|
||||
cx.spawn(
|
||||
|cx| async move { client.authenticate_and_connect(true, &cx).log_err().await },
|
||||
)
|
||||
.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();
|
||||
if let Some(client) = client.upgrade() {
|
||||
cx.spawn(|cx| async move {
|
||||
client.disconnect(&cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -221,7 +229,7 @@ enum WeakSubscriber {
|
||||
|
||||
enum Subscriber {
|
||||
Model(AnyModelHandle),
|
||||
View(AnyViewHandle),
|
||||
View(AnyWeakViewHandle),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -325,6 +333,42 @@ impl<T: Entity> Drop for PendingEntitySubscription<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct TelemetrySettings {
|
||||
pub diagnostics: bool,
|
||||
pub metrics: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct TelemetrySettingsContent {
|
||||
pub diagnostics: Option<bool>,
|
||||
pub metrics: Option<bool>,
|
||||
}
|
||||
|
||||
impl settings::Setting for TelemetrySettings {
|
||||
const KEY: Option<&'static str> = Some("telemetry");
|
||||
|
||||
type FileContent = TelemetrySettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &AppContext,
|
||||
) -> Result<Self> {
|
||||
Ok(Self {
|
||||
diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
|
||||
default_value
|
||||
.diagnostics
|
||||
.ok_or_else(Self::missing_default)?,
|
||||
),
|
||||
metrics: user_values
|
||||
.first()
|
||||
.and_then(|v| v.metrics)
|
||||
.unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
@@ -446,9 +490,7 @@ impl Client {
|
||||
}));
|
||||
}
|
||||
Status::SignedOut | Status::UpgradeRequired => {
|
||||
let telemetry_settings = cx.read(|cx| cx.global::<Settings>().telemetry());
|
||||
self.telemetry
|
||||
.set_authenticated_user_info(None, false, telemetry_settings);
|
||||
cx.read(|cx| self.telemetry.set_authenticated_user_info(None, false, cx));
|
||||
state._reconnect_task.take();
|
||||
}
|
||||
_ => {}
|
||||
@@ -567,7 +609,7 @@ impl Client {
|
||||
H: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
+ Fn(WeakViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
F: 'static + Future<Output = Result<()>>,
|
||||
{
|
||||
self.add_entity_message_handler::<M, E, _, _>(move |handle, message, client, cx| {
|
||||
@@ -666,7 +708,7 @@ impl Client {
|
||||
H: 'static
|
||||
+ Send
|
||||
+ Sync
|
||||
+ Fn(ViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
+ Fn(WeakViewHandle<E>, TypedEnvelope<M>, Arc<Self>, AsyncAppContext) -> F,
|
||||
F: 'static + Future<Output = Result<M::Response>>,
|
||||
{
|
||||
self.add_view_message_handler(move |entity, envelope, client, cx| {
|
||||
@@ -736,10 +778,10 @@ impl Client {
|
||||
read_from_keychain = credentials.is_some();
|
||||
if read_from_keychain {
|
||||
cx.read(|cx| {
|
||||
self.report_event(
|
||||
self.telemetry().report_mixpanel_event(
|
||||
"read credentials from keychain",
|
||||
Default::default(),
|
||||
cx.global::<Settings>().telemetry(),
|
||||
*settings::get::<TelemetrySettings>(cx),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1032,7 +1074,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());
|
||||
|
||||
let telemetry_settings = cx.read(|cx| *settings::get::<TelemetrySettings>(cx));
|
||||
|
||||
executor.clone().spawn(async move {
|
||||
// Generate a pair of asymmetric encryption keys. The public key will be used by the
|
||||
@@ -1116,10 +1159,10 @@ impl Client {
|
||||
.context("failed to decrypt access token")?;
|
||||
platform.activate(true);
|
||||
|
||||
telemetry.report_event(
|
||||
telemetry.report_mixpanel_event(
|
||||
"authenticate with browser",
|
||||
Default::default(),
|
||||
metrics_enabled,
|
||||
telemetry_settings,
|
||||
);
|
||||
|
||||
Ok(Credentials {
|
||||
@@ -1273,7 +1316,15 @@ impl Client {
|
||||
pending.push(message);
|
||||
return;
|
||||
}
|
||||
Some(weak_subscriber @ _) => subscriber = weak_subscriber.upgrade(cx),
|
||||
Some(weak_subscriber @ _) => match weak_subscriber {
|
||||
WeakSubscriber::Model(handle) => {
|
||||
subscriber = handle.upgrade(cx).map(Subscriber::Model);
|
||||
}
|
||||
WeakSubscriber::View(handle) => {
|
||||
subscriber = Some(Subscriber::View(handle.clone()));
|
||||
}
|
||||
WeakSubscriber::Pending(_) => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1330,40 +1381,8 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_telemetry(&self) {
|
||||
self.telemetry.start();
|
||||
}
|
||||
|
||||
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 {
|
||||
fn upgrade(&self, cx: &AsyncAppContext) -> Option<Subscriber> {
|
||||
match self {
|
||||
WeakSubscriber::Model(handle) => handle.upgrade(cx).map(Subscriber::Model),
|
||||
WeakSubscriber::View(handle) => handle.upgrade(cx).map(Subscriber::View),
|
||||
WeakSubscriber::Pending(_) => None,
|
||||
}
|
||||
pub fn telemetry(&self) -> &Arc<Telemetry> {
|
||||
&self.telemetry
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
executor::Background,
|
||||
@@ -8,8 +9,8 @@ use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use serde_json::json;
|
||||
use settings::TelemetrySettings;
|
||||
use std::{
|
||||
env,
|
||||
io::Write,
|
||||
mem,
|
||||
path::PathBuf,
|
||||
@@ -29,26 +30,69 @@ pub struct Telemetry {
|
||||
|
||||
#[derive(Default)]
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>,
|
||||
device_id: Option<Arc<str>>,
|
||||
metrics_id: Option<Arc<str>>, // Per logged-in user
|
||||
installation_id: Option<Arc<str>>, // Per app installation
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
os_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
queue: Vec<MixpanelEvent>,
|
||||
next_event_id: usize,
|
||||
flush_task: Option<Task<()>>,
|
||||
os_version: Option<Arc<str>>,
|
||||
architecture: &'static str,
|
||||
mixpanel_events_queue: Vec<MixpanelEvent>,
|
||||
clickhouse_events_queue: Vec<ClickhouseEventWrapper>,
|
||||
next_mixpanel_event_id: usize,
|
||||
flush_mixpanel_events_task: Option<Task<()>>,
|
||||
flush_clickhouse_events_task: Option<Task<()>>,
|
||||
log_file: Option<NamedTempFile>,
|
||||
is_staff: Option<bool>,
|
||||
}
|
||||
|
||||
const MIXPANEL_EVENTS_URL: &'static str = "https://api.mixpanel.com/track";
|
||||
const MIXPANEL_ENGAGE_URL: &'static str = "https://api.mixpanel.com/engage#profile-set";
|
||||
const CLICKHOUSE_EVENTS_URL_PATH: &'static str = "/api/events";
|
||||
|
||||
lazy_static! {
|
||||
static ref MIXPANEL_TOKEN: Option<String> = std::env::var("ZED_MIXPANEL_TOKEN")
|
||||
.ok()
|
||||
.or_else(|| option_env!("ZED_MIXPANEL_TOKEN").map(|key| key.to_string()));
|
||||
static ref CLICKHOUSE_EVENTS_URL: String =
|
||||
format!("{}{}", *ZED_SERVER_URL, CLICKHOUSE_EVENTS_URL_PATH);
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ClickhouseEventRequestBody {
|
||||
token: &'static str,
|
||||
installation_id: Option<Arc<str>>,
|
||||
app_version: Option<Arc<str>>,
|
||||
os_name: &'static str,
|
||||
os_version: Option<Arc<str>>,
|
||||
architecture: &'static str,
|
||||
release_channel: Option<&'static str>,
|
||||
events: Vec<ClickhouseEventWrapper>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
struct ClickhouseEventWrapper {
|
||||
time: u128,
|
||||
signed_in: bool,
|
||||
#[serde(flatten)]
|
||||
event: ClickhouseEvent,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ClickhouseEvent {
|
||||
Editor {
|
||||
operation: &'static str,
|
||||
file_extension: Option<String>,
|
||||
vim_mode: bool,
|
||||
copilot_enabled: bool,
|
||||
copilot_enabled_for_language: bool,
|
||||
},
|
||||
Copilot {
|
||||
suggestion_id: Option<String>,
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
@@ -63,7 +107,8 @@ struct MixpanelEventProperties {
|
||||
#[serde(skip_serializing_if = "str::is_empty")]
|
||||
token: &'static str,
|
||||
time: u128,
|
||||
distinct_id: Option<Arc<str>>,
|
||||
#[serde(rename = "distinct_id")]
|
||||
installation_id: Option<Arc<str>>,
|
||||
#[serde(rename = "$insert_id")]
|
||||
insert_id: usize,
|
||||
// Custom fields
|
||||
@@ -86,7 +131,7 @@ struct MixpanelEngageRequest {
|
||||
#[serde(rename = "$token")]
|
||||
token: &'static str,
|
||||
#[serde(rename = "$distinct_id")]
|
||||
distinct_id: Arc<str>,
|
||||
installation_id: Arc<str>,
|
||||
#[serde(rename = "$set")]
|
||||
set: Value,
|
||||
}
|
||||
@@ -111,19 +156,23 @@ impl Telemetry {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// TODO: Replace all hardware stuff with nested SystemSpecs json
|
||||
let this = Arc::new(Self {
|
||||
http_client: client,
|
||||
executor: cx.background().clone(),
|
||||
state: Mutex::new(TelemetryState {
|
||||
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
|
||||
os_name: platform.os_name().into(),
|
||||
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
|
||||
architecture: env::consts::ARCH,
|
||||
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
|
||||
release_channel,
|
||||
device_id: None,
|
||||
installation_id: None,
|
||||
metrics_id: None,
|
||||
queue: Default::default(),
|
||||
flush_task: Default::default(),
|
||||
next_event_id: 0,
|
||||
mixpanel_events_queue: Default::default(),
|
||||
clickhouse_events_queue: Default::default(),
|
||||
flush_mixpanel_events_task: Default::default(),
|
||||
flush_clickhouse_events_task: Default::default(),
|
||||
next_mixpanel_event_id: 0,
|
||||
log_file: None,
|
||||
is_staff: None,
|
||||
}),
|
||||
@@ -154,29 +203,38 @@ impl Telemetry {
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let device_id =
|
||||
if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
|
||||
device_id
|
||||
let installation_id =
|
||||
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") {
|
||||
installation_id
|
||||
} else {
|
||||
let device_id = Uuid::new_v4().to_string();
|
||||
let installation_id = Uuid::new_v4().to_string();
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp("device_id".to_string(), device_id.clone())
|
||||
.write_kvp("device_id".to_string(), installation_id.clone())
|
||||
.await?;
|
||||
device_id
|
||||
installation_id
|
||||
};
|
||||
|
||||
let device_id: Arc<str> = device_id.into();
|
||||
let installation_id: Arc<str> = installation_id.into();
|
||||
let mut state = this.state.lock();
|
||||
state.device_id = Some(device_id.clone());
|
||||
for event in &mut state.queue {
|
||||
state.installation_id = Some(installation_id.clone());
|
||||
|
||||
for event in &mut state.mixpanel_events_queue {
|
||||
event
|
||||
.properties
|
||||
.distinct_id
|
||||
.get_or_insert_with(|| device_id.clone());
|
||||
.installation_id
|
||||
.get_or_insert_with(|| installation_id.clone());
|
||||
}
|
||||
if !state.queue.is_empty() {
|
||||
drop(state);
|
||||
this.flush();
|
||||
|
||||
let has_mixpanel_events = !state.mixpanel_events_queue.is_empty();
|
||||
let has_clickhouse_events = !state.clickhouse_events_queue.is_empty();
|
||||
drop(state);
|
||||
|
||||
if has_mixpanel_events {
|
||||
this.flush_mixpanel_events();
|
||||
}
|
||||
|
||||
if has_clickhouse_events {
|
||||
this.flush_clickhouse_events();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -192,27 +250,27 @@ impl Telemetry {
|
||||
self: &Arc<Self>,
|
||||
metrics_id: Option<String>,
|
||||
is_staff: bool,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
if !telemetry_settings.metrics() {
|
||||
if !settings::get::<TelemetrySettings>(cx).metrics {
|
||||
return;
|
||||
}
|
||||
|
||||
let this = self.clone();
|
||||
let mut state = self.state.lock();
|
||||
let device_id = state.device_id.clone();
|
||||
let installation_id = state.installation_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) {
|
||||
if let Some((token, installation_id)) = MIXPANEL_TOKEN.as_ref().zip(installation_id) {
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
|
||||
token,
|
||||
distinct_id: device_id,
|
||||
installation_id,
|
||||
set: json!({
|
||||
"Staff": is_staff,
|
||||
"ID": metrics_id,
|
||||
@@ -231,27 +289,62 @@ impl Telemetry {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_event(
|
||||
pub fn report_clickhouse_event(
|
||||
self: &Arc<Self>,
|
||||
event: ClickhouseEvent,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let signed_in = state.metrics_id.is_some();
|
||||
state.clickhouse_events_queue.push(ClickhouseEventWrapper {
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
signed_in,
|
||||
event,
|
||||
});
|
||||
|
||||
if state.installation_id.is_some() {
|
||||
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush_clickhouse_events();
|
||||
} else {
|
||||
let this = self.clone();
|
||||
let executor = self.executor.clone();
|
||||
state.flush_clickhouse_events_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||
this.flush_clickhouse_events();
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_mixpanel_event(
|
||||
self: &Arc<Self>,
|
||||
kind: &str,
|
||||
properties: Value,
|
||||
telemetry_settings: TelemetrySettings,
|
||||
) {
|
||||
if !telemetry_settings.metrics() {
|
||||
if !telemetry_settings.metrics {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut state = self.state.lock();
|
||||
let event = MixpanelEvent {
|
||||
event: kind.to_string(),
|
||||
event: kind.into(),
|
||||
properties: MixpanelEventProperties {
|
||||
token: "",
|
||||
time: SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis(),
|
||||
distinct_id: state.device_id.clone(),
|
||||
insert_id: post_inc(&mut state.next_event_id),
|
||||
installation_id: state.installation_id.clone(),
|
||||
insert_id: post_inc(&mut state.next_mixpanel_event_id),
|
||||
event_properties: if let Value::Object(properties) = properties {
|
||||
Some(properties)
|
||||
} else {
|
||||
@@ -264,17 +357,17 @@ impl Telemetry {
|
||||
signed_in: state.metrics_id.is_some(),
|
||||
},
|
||||
};
|
||||
state.queue.push(event);
|
||||
if state.device_id.is_some() {
|
||||
if state.queue.len() >= MAX_QUEUE_LEN {
|
||||
state.mixpanel_events_queue.push(event);
|
||||
if state.installation_id.is_some() {
|
||||
if state.mixpanel_events_queue.len() >= MAX_QUEUE_LEN {
|
||||
drop(state);
|
||||
self.flush();
|
||||
self.flush_mixpanel_events();
|
||||
} else {
|
||||
let this = self.clone();
|
||||
let executor = self.executor.clone();
|
||||
state.flush_task = Some(self.executor.spawn(async move {
|
||||
state.flush_mixpanel_events_task = Some(self.executor.spawn(async move {
|
||||
executor.timer(DEBOUNCE_INTERVAL).await;
|
||||
this.flush();
|
||||
this.flush_mixpanel_events();
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -284,14 +377,18 @@ impl Telemetry {
|
||||
self.state.lock().metrics_id.clone()
|
||||
}
|
||||
|
||||
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
|
||||
self.state.lock().installation_id.clone()
|
||||
}
|
||||
|
||||
pub fn is_staff(self: &Arc<Self>) -> Option<bool> {
|
||||
self.state.lock().is_staff
|
||||
}
|
||||
|
||||
fn flush(self: &Arc<Self>) {
|
||||
fn flush_mixpanel_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.queue);
|
||||
state.flush_task.take();
|
||||
let mut events = mem::take(&mut state.mixpanel_events_queue);
|
||||
state.flush_mixpanel_events_task.take();
|
||||
drop(state);
|
||||
|
||||
if let Some(token) = MIXPANEL_TOKEN.as_ref() {
|
||||
@@ -325,4 +422,55 @@ impl Telemetry {
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_clickhouse_events(self: &Arc<Self>) {
|
||||
let mut state = self.state.lock();
|
||||
let mut events = mem::take(&mut state.clickhouse_events_queue);
|
||||
state.flush_clickhouse_events_task.take();
|
||||
drop(state);
|
||||
|
||||
let this = self.clone();
|
||||
self.executor
|
||||
.spawn(
|
||||
async move {
|
||||
let mut json_bytes = Vec::new();
|
||||
|
||||
if let Some(file) = &mut this.state.lock().log_file {
|
||||
let file = file.as_file_mut();
|
||||
for event in &mut events {
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(&mut json_bytes, event)?;
|
||||
file.write_all(&json_bytes)?;
|
||||
file.write(b"\n")?;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let state = this.state.lock();
|
||||
json_bytes.clear();
|
||||
serde_json::to_writer(
|
||||
&mut json_bytes,
|
||||
&ClickhouseEventRequestBody {
|
||||
token: ZED_SECRET_CLIENT_TOKEN,
|
||||
installation_id: state.installation_id.clone(),
|
||||
app_version: state.app_version.clone(),
|
||||
os_name: state.os_name,
|
||||
os_version: state.os_version.clone(),
|
||||
architecture: state.architecture,
|
||||
|
||||
release_channel: state.release_channel,
|
||||
events,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
this.http_client
|
||||
.post_json(CLICKHOUSE_EVENTS_URL.as_str(), json_bytes.into())
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err(),
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ 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 staff_mode::StaffMode;
|
||||
use std::sync::{Arc, Weak};
|
||||
use util::http::HttpClient;
|
||||
@@ -144,11 +143,13 @@ impl UserStore {
|
||||
let fetch_metrics_id =
|
||||
client.request(proto::GetPrivateUserInfo {}).log_err();
|
||||
let (user, info) = futures::join!(fetch_user, fetch_metrics_id);
|
||||
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.read(|cx| {
|
||||
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,
|
||||
)
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_default_global(|staff_mode: &mut StaffMode, _| {
|
||||
|
||||
@@ -9,4 +9,4 @@ path = "src/clock.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.8.2"
|
||||
version = "0.12.4"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
@@ -19,7 +19,7 @@ live_kit_server = { path = "../live_kit_server" }
|
||||
rpc = { path = "../rpc" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow = "1.0.40"
|
||||
anyhow.workspace = true
|
||||
async-tungstenite = "0.16"
|
||||
axum = { version = "0.5", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.3", features = ["erased-json"] }
|
||||
@@ -27,31 +27,31 @@ base64 = "0.13"
|
||||
clap = { version = "3.1", features = ["derive"], optional = true }
|
||||
dashmap = "5.4"
|
||||
envy = "0.4.2"
|
||||
futures = "0.3"
|
||||
futures.workspace = true
|
||||
hyper = "0.14"
|
||||
lazy_static = "1.4"
|
||||
lazy_static.workspace = true
|
||||
lipsum = { version = "0.8", optional = true }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
log.workspace = true
|
||||
nanoid = "0.4"
|
||||
parking_lot = "0.11.1"
|
||||
parking_lot.workspace = true
|
||||
prometheus = "0.13"
|
||||
rand = "0.8"
|
||||
rand.workspace = true
|
||||
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||
scrypt = "0.7"
|
||||
# Remove fork dependency when a version with https://github.com/SeaQL/sea-orm/pull/1283 is released.
|
||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-postgres", "postgres-array", "runtime-tokio-rustls"] }
|
||||
sea-query = "0.27"
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
sha-1 = "0.9"
|
||||
sqlx = { version = "0.6", features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid", "any"] }
|
||||
time = { version = "0.3", features = ["serde", "serde-well-known"] }
|
||||
time.workspace = true
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tokio-tungstenite = "0.17"
|
||||
tonic = "0.6"
|
||||
tower = "0.4"
|
||||
toml = "0.5.8"
|
||||
toml.workspace = true
|
||||
tracing = "0.1.34"
|
||||
tracing-log = "0.1.3"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||
@@ -74,14 +74,15 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||
theme = { path = "../theme" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
ctor = "0.1"
|
||||
env_logger = "0.9"
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
indoc.workspace = true
|
||||
util = { path = "../util" }
|
||||
lazy_static = "1.4"
|
||||
lazy_static.workspace = true
|
||||
sea-orm = { git = "https://github.com/zed-industries/sea-orm", rev = "18f4c691085712ad014a51792af75a9044bacee6", features = ["sqlx-sqlite"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
sqlx = { version = "0.6", features = ["sqlite"] }
|
||||
unindent = "0.1"
|
||||
unindent.workspace = true
|
||||
|
||||
[features]
|
||||
seed-support = ["clap", "lipsum", "reqwest"]
|
||||
|
||||
@@ -82,6 +82,37 @@ CREATE TABLE "worktree_entries" (
|
||||
CREATE INDEX "index_worktree_entries_on_project_id" ON "worktree_entries" ("project_id");
|
||||
CREATE INDEX "index_worktree_entries_on_project_id_and_worktree_id" ON "worktree_entries" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_repositories" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"work_directory_id" INTEGER NOT NULL,
|
||||
"branch" VARCHAR,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
|
||||
|
||||
CREATE TABLE "worktree_repository_statuses" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
"work_directory_id" INTEGER NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INTEGER NOT NULL,
|
||||
"scan_id" INTEGER NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
|
||||
CREATE INDEX "index_worktree_repository_statuses_on_project_id_and_worktree_id_and_work_directory_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
|
||||
|
||||
|
||||
CREATE TABLE "worktree_diagnostic_summaries" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INTEGER NOT NULL,
|
||||
@@ -153,7 +184,7 @@ CREATE TABLE "followers" (
|
||||
"follower_connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
|
||||
"follower_connection_id" INTEGER NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX
|
||||
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,13 @@
|
||||
CREATE TABLE "worktree_repositories" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"work_directory_id" INT8 NOT NULL,
|
||||
"scan_id" INT8 NOT NULL,
|
||||
"branch" VARCHAR,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id" ON "worktree_repositories" ("project_id");
|
||||
CREATE INDEX "index_worktree_repositories_on_project_id_and_worktree_id" ON "worktree_repositories" ("project_id", "worktree_id");
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE "worktree_repository_statuses" (
|
||||
"project_id" INTEGER NOT NULL,
|
||||
"worktree_id" INT8 NOT NULL,
|
||||
"work_directory_id" INT8 NOT NULL,
|
||||
"repo_path" VARCHAR NOT NULL,
|
||||
"status" INT8 NOT NULL,
|
||||
"scan_id" INT8 NOT NULL,
|
||||
"is_deleted" BOOL NOT NULL,
|
||||
PRIMARY KEY(project_id, worktree_id, work_directory_id, repo_path),
|
||||
FOREIGN KEY(project_id, worktree_id) REFERENCES worktrees (project_id, id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(project_id, worktree_id, work_directory_id) REFERENCES worktree_entries (project_id, worktree_id, id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id" ON "worktree_repository_statuses" ("project_id");
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id" ON "worktree_repository_statuses" ("project_id", "worktree_id");
|
||||
CREATE INDEX "index_wt_repos_statuses_on_project_id_and_wt_id_and_wd_id" ON "worktree_repository_statuses" ("project_id", "worktree_id", "work_directory_id");
|
||||
@@ -14,6 +14,8 @@ mod user;
|
||||
mod worktree;
|
||||
mod worktree_diagnostic_summary;
|
||||
mod worktree_entry;
|
||||
mod worktree_repository;
|
||||
mod worktree_repository_statuses;
|
||||
|
||||
use crate::executor::Executor;
|
||||
use crate::{Error, Result};
|
||||
@@ -1489,6 +1491,8 @@ impl Database {
|
||||
visible: db_worktree.visible,
|
||||
updated_entries: Default::default(),
|
||||
removed_entries: Default::default(),
|
||||
updated_repositories: Default::default(),
|
||||
removed_repositories: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
@@ -1498,38 +1502,125 @@ impl Database {
|
||||
.worktrees
|
||||
.iter()
|
||||
.find(|worktree| worktree.id == db_worktree.id as u64);
|
||||
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_entry::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_entries = worktree_entry::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
|
||||
.add(entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_entry) = db_entries.next().await {
|
||||
let db_entry = db_entry?;
|
||||
if db_entry.is_deleted {
|
||||
worktree.removed_entries.push(db_entry.id as u64);
|
||||
// File entries
|
||||
{
|
||||
let entry_filter = if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_entry::Column::ScanId.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree.updated_entries.push(proto::Entry {
|
||||
id: db_entry.id as u64,
|
||||
is_dir: db_entry.is_dir,
|
||||
path: db_entry.path,
|
||||
inode: db_entry.inode as u64,
|
||||
mtime: Some(proto::Timestamp {
|
||||
seconds: db_entry.mtime_seconds as u64,
|
||||
nanos: db_entry.mtime_nanos as u32,
|
||||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
});
|
||||
worktree_entry::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_entries = worktree_entry::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_entry::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_entry::Column::WorktreeId.eq(worktree.id))
|
||||
.add(entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_entry) = db_entries.next().await {
|
||||
let db_entry = db_entry?;
|
||||
if db_entry.is_deleted {
|
||||
worktree.removed_entries.push(db_entry.id as u64);
|
||||
} else {
|
||||
worktree.updated_entries.push(proto::Entry {
|
||||
id: db_entry.id as u64,
|
||||
is_dir: db_entry.is_dir,
|
||||
path: db_entry.path,
|
||||
inode: db_entry.inode as u64,
|
||||
mtime: Some(proto::Timestamp {
|
||||
seconds: db_entry.mtime_seconds as u64,
|
||||
nanos: db_entry.mtime_nanos as u32,
|
||||
}),
|
||||
is_symlink: db_entry.is_symlink,
|
||||
is_ignored: db_entry.is_ignored,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Repository Entries
|
||||
{
|
||||
let repository_entry_filter =
|
||||
if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_repository::Column::ScanId.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_repository::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_repositories = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project.id))
|
||||
.add(worktree_repository::Column::WorktreeId.eq(worktree.id))
|
||||
.add(repository_entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_repository) = db_repositories.next().await {
|
||||
let db_repository = db_repository?;
|
||||
if db_repository.is_deleted {
|
||||
worktree
|
||||
.removed_repositories
|
||||
.push(db_repository.work_directory_id as u64);
|
||||
} else {
|
||||
worktree.updated_repositories.push(proto::RepositoryEntry {
|
||||
work_directory_id: db_repository.work_directory_id as u64,
|
||||
branch: db_repository.branch,
|
||||
removed_repo_paths: Default::default(),
|
||||
updated_statuses: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Repository Status Entries
|
||||
for repository in worktree.updated_repositories.iter_mut() {
|
||||
let repository_status_entry_filter =
|
||||
if let Some(rejoined_worktree) = rejoined_worktree {
|
||||
worktree_repository_statuses::Column::ScanId
|
||||
.gt(rejoined_worktree.scan_id)
|
||||
} else {
|
||||
worktree_repository_statuses::Column::IsDeleted.eq(false)
|
||||
};
|
||||
|
||||
let mut db_repository_statuses =
|
||||
worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree.id),
|
||||
)
|
||||
.add(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(repository.work_directory_id),
|
||||
)
|
||||
.add(repository_status_entry_filter),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_status_entry) = db_repository_statuses.next().await {
|
||||
let db_status_entry = db_status_entry?;
|
||||
if db_status_entry.is_deleted {
|
||||
repository
|
||||
.removed_repo_paths
|
||||
.push(db_status_entry.repo_path);
|
||||
} else {
|
||||
repository.updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status_entry.repo_path,
|
||||
status: db_status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2330,6 +2421,115 @@ impl Database {
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !update.updated_repositories.is_empty() {
|
||||
worktree_repository::Entity::insert_many(update.updated_repositories.iter().map(
|
||||
|repository| worktree_repository::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
worktree_id: ActiveValue::set(worktree_id),
|
||||
work_directory_id: ActiveValue::set(repository.work_directory_id as i64),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
branch: ActiveValue::set(repository.branch.clone()),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
},
|
||||
))
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_repository::Column::ProjectId,
|
||||
worktree_repository::Column::WorktreeId,
|
||||
worktree_repository::Column::WorkDirectoryId,
|
||||
])
|
||||
.update_columns([
|
||||
worktree_repository::Column::ScanId,
|
||||
worktree_repository::Column::Branch,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
for repository in update.updated_repositories.iter() {
|
||||
if !repository.updated_statuses.is_empty() {
|
||||
worktree_repository_statuses::Entity::insert_many(
|
||||
repository.updated_statuses.iter().map(|status_entry| {
|
||||
worktree_repository_statuses::ActiveModel {
|
||||
project_id: ActiveValue::set(project_id),
|
||||
worktree_id: ActiveValue::set(worktree_id),
|
||||
work_directory_id: ActiveValue::set(
|
||||
repository.work_directory_id as i64,
|
||||
),
|
||||
repo_path: ActiveValue::set(status_entry.repo_path.clone()),
|
||||
status: ActiveValue::set(status_entry.status as i64),
|
||||
scan_id: ActiveValue::set(update.scan_id as i64),
|
||||
is_deleted: ActiveValue::set(false),
|
||||
}
|
||||
}),
|
||||
)
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
worktree_repository_statuses::Column::ProjectId,
|
||||
worktree_repository_statuses::Column::WorktreeId,
|
||||
worktree_repository_statuses::Column::WorkDirectoryId,
|
||||
worktree_repository_statuses::Column::RepoPath,
|
||||
])
|
||||
.update_columns([
|
||||
worktree_repository_statuses::Column::ScanId,
|
||||
worktree_repository_statuses::Column::Status,
|
||||
worktree_repository_statuses::Column::IsDeleted,
|
||||
])
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !repository.removed_repo_paths.is_empty() {
|
||||
worktree_repository_statuses::Entity::update_many()
|
||||
.filter(
|
||||
worktree_repository_statuses::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::WorktreeId
|
||||
.eq(worktree_id),
|
||||
)
|
||||
.and(
|
||||
worktree_repository_statuses::Column::WorkDirectoryId
|
||||
.eq(repository.work_directory_id as i64),
|
||||
)
|
||||
.and(worktree_repository_statuses::Column::RepoPath.is_in(
|
||||
repository.removed_repo_paths.iter().map(String::as_str),
|
||||
)),
|
||||
)
|
||||
.set(worktree_repository_statuses::ActiveModel {
|
||||
is_deleted: ActiveValue::Set(true),
|
||||
scan_id: ActiveValue::Set(update.scan_id as i64),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !update.removed_repositories.is_empty() {
|
||||
worktree_repository::Entity::update_many()
|
||||
.filter(
|
||||
worktree_repository::Column::ProjectId
|
||||
.eq(project_id)
|
||||
.and(worktree_repository::Column::WorktreeId.eq(worktree_id))
|
||||
.and(
|
||||
worktree_repository::Column::WorkDirectoryId
|
||||
.is_in(update.removed_repositories.iter().map(|id| *id as i64)),
|
||||
),
|
||||
)
|
||||
.set(worktree_repository::ActiveModel {
|
||||
is_deleted: ActiveValue::Set(true),
|
||||
scan_id: ActiveValue::Set(update.scan_id as i64),
|
||||
..Default::default()
|
||||
})
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let connection_ids = self.project_guest_connection_ids(project_id, &tx).await?;
|
||||
Ok(connection_ids)
|
||||
})
|
||||
@@ -2505,6 +2705,7 @@ impl Database {
|
||||
root_name: db_worktree.root_name,
|
||||
visible: db_worktree.visible,
|
||||
entries: Default::default(),
|
||||
repository_entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
scan_id: db_worktree.scan_id as u64,
|
||||
completed_scan_id: db_worktree.completed_scan_id as u64,
|
||||
@@ -2542,6 +2743,61 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
// Populate repository entries.
|
||||
{
|
||||
let mut db_repository_entries = worktree_repository::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository::Column::ProjectId.eq(project_id))
|
||||
.add(worktree_repository::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
while let Some(db_repository_entry) = db_repository_entries.next().await {
|
||||
let db_repository_entry = db_repository_entry?;
|
||||
if let Some(worktree) =
|
||||
worktrees.get_mut(&(db_repository_entry.worktree_id as u64))
|
||||
{
|
||||
worktree.repository_entries.insert(
|
||||
db_repository_entry.work_directory_id as u64,
|
||||
proto::RepositoryEntry {
|
||||
work_directory_id: db_repository_entry.work_directory_id as u64,
|
||||
branch: db_repository_entry.branch,
|
||||
removed_repo_paths: Default::default(),
|
||||
updated_statuses: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut db_status_entries = worktree_repository_statuses::Entity::find()
|
||||
.filter(
|
||||
Condition::all()
|
||||
.add(worktree_repository_statuses::Column::ProjectId.eq(project_id))
|
||||
.add(worktree_repository_statuses::Column::IsDeleted.eq(false)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
while let Some(db_status_entry) = db_status_entries.next().await {
|
||||
let db_status_entry = db_status_entry?;
|
||||
if let Some(worktree) = worktrees.get_mut(&(db_status_entry.worktree_id as u64))
|
||||
{
|
||||
if let Some(repository_entry) = worktree
|
||||
.repository_entries
|
||||
.get_mut(&(db_status_entry.work_directory_id as u64))
|
||||
{
|
||||
repository_entry.updated_statuses.push(proto::StatusEntry {
|
||||
repo_path: db_status_entry.repo_path,
|
||||
status: db_status_entry.status as i32,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Populate worktree diagnostic summaries.
|
||||
{
|
||||
let mut db_summaries = worktree_diagnostic_summary::Entity::find()
|
||||
@@ -3223,6 +3479,8 @@ pub struct RejoinedWorktree {
|
||||
pub visible: bool,
|
||||
pub updated_entries: Vec<proto::Entry>,
|
||||
pub removed_entries: Vec<u64>,
|
||||
pub updated_repositories: Vec<proto::RepositoryEntry>,
|
||||
pub removed_repositories: Vec<u64>,
|
||||
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
|
||||
pub scan_id: u64,
|
||||
pub completed_scan_id: u64,
|
||||
@@ -3277,6 +3535,7 @@ pub struct Worktree {
|
||||
pub root_name: String,
|
||||
pub visible: bool,
|
||||
pub entries: Vec<proto::Entry>,
|
||||
pub repository_entries: BTreeMap<u64, proto::RepositoryEntry>,
|
||||
pub diagnostic_summaries: Vec<proto::DiagnosticSummary>,
|
||||
pub scan_id: u64,
|
||||
pub completed_scan_id: u64,
|
||||
|
||||
21
crates/collab/src/db/worktree_repository.rs
Normal file
21
crates/collab/src/db/worktree_repository.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktree_repositories")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub worktree_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub work_directory_id: i64,
|
||||
pub scan_id: i64,
|
||||
pub branch: Option<String>,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
23
crates/collab/src/db/worktree_repository_statuses.rs
Normal file
23
crates/collab/src/db/worktree_repository_statuses.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use super::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "worktree_repository_statuses")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
#[sea_orm(primary_key)]
|
||||
pub worktree_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub work_directory_id: i64,
|
||||
#[sea_orm(primary_key)]
|
||||
pub repo_path: String,
|
||||
pub status: i64,
|
||||
pub scan_id: i64,
|
||||
pub is_deleted: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -51,7 +51,7 @@ use std::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::{watch, Semaphore};
|
||||
use tower::ServiceBuilder;
|
||||
@@ -223,6 +223,7 @@ impl Server {
|
||||
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::CopyProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(update_buffer_file)
|
||||
@@ -397,10 +398,16 @@ impl Server {
|
||||
"message received"
|
||||
);
|
||||
});
|
||||
let start_time = Instant::now();
|
||||
let future = (handler)(*envelope, session);
|
||||
async move {
|
||||
if let Err(error) = future.await {
|
||||
tracing::error!(%error, "error handling message");
|
||||
let result = future.await;
|
||||
let duration_ms = start_time.elapsed().as_micros() as f64 / 1000.0;
|
||||
match result {
|
||||
Err(error) => {
|
||||
tracing::error!(%error, ?duration_ms, "error handling message")
|
||||
}
|
||||
Ok(()) => tracing::info!(?duration_ms, "finished handling message"),
|
||||
}
|
||||
}
|
||||
.instrument(span)
|
||||
@@ -1063,6 +1070,8 @@ async fn rejoin_room(
|
||||
removed_entries: worktree.removed_entries,
|
||||
scan_id: worktree.scan_id,
|
||||
is_last_update: worktree.completed_scan_id == worktree.scan_id,
|
||||
updated_repositories: worktree.updated_repositories,
|
||||
removed_repositories: worktree.removed_repositories,
|
||||
};
|
||||
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||
session.peer.send(session.connection_id, update.clone())?;
|
||||
@@ -1383,6 +1392,8 @@ async fn join_project(
|
||||
removed_entries: Default::default(),
|
||||
scan_id: worktree.scan_id,
|
||||
is_last_update: worktree.scan_id == worktree.completed_scan_id,
|
||||
updated_repositories: worktree.repository_entries.into_values().collect(),
|
||||
removed_repositories: Default::default(),
|
||||
};
|
||||
for update in proto::split_worktree_update(message, MAX_CHUNK_SIZE) {
|
||||
session.peer.send(session.connection_id, update.clone())?;
|
||||
|
||||
@@ -12,11 +12,14 @@ use client::{
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::FakeFs;
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{executor::Deterministic, test::EmptyView, ModelHandle, TestAppContext, ViewHandle};
|
||||
use gpui::{
|
||||
elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use parking_lot::Mutex;
|
||||
use project::{Project, WorktreeId};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
env,
|
||||
@@ -27,7 +30,6 @@ use std::{
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use theme::ThemeRegistry;
|
||||
use util::http::FakeHttpClient;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -99,7 +101,7 @@ impl TestServer {
|
||||
|
||||
async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(Settings::test(cx));
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
});
|
||||
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
@@ -188,16 +190,18 @@ impl TestServer {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
languages: Arc::new(LanguageRegistry::test()),
|
||||
themes: ThemeRegistry::new((), cx.font_cache()),
|
||||
fs: fs.clone(),
|
||||
build_window_options: |_, _, _| Default::default(),
|
||||
initialize_workspace: |_, _, _| unimplemented!(),
|
||||
dock_default_item_factory: |_, _| None,
|
||||
initialize_workspace: |_, _, _, _| unimplemented!(),
|
||||
background_actions: || &[],
|
||||
});
|
||||
|
||||
Project::init(&client);
|
||||
cx.update(|cx| {
|
||||
theme::init((), cx);
|
||||
Project::init(&client, cx);
|
||||
client::init(&client, cx);
|
||||
language::init(cx);
|
||||
editor::init_settings(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
call::init(client.clone(), user_store.clone(), cx);
|
||||
});
|
||||
@@ -462,8 +466,41 @@ impl TestClient {
|
||||
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))
|
||||
struct WorkspaceContainer {
|
||||
workspace: Option<WeakViewHandle<Workspace>>,
|
||||
}
|
||||
|
||||
impl Entity for WorkspaceContainer {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for WorkspaceContainer {
|
||||
fn ui_name() -> &'static str {
|
||||
"WorkspaceContainer"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if let Some(workspace) = self
|
||||
.workspace
|
||||
.as_ref()
|
||||
.and_then(|workspace| workspace.upgrade(cx))
|
||||
{
|
||||
ChildView::new(&workspace, cx).into_any()
|
||||
} else {
|
||||
Empty::new().into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We use a workspace container so that we don't need to remove the window in order to
|
||||
// drop the workspace and we can use a ViewHandle instead.
|
||||
let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None });
|
||||
let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx));
|
||||
container.update(cx, |container, cx| {
|
||||
container.workspace = Some(workspace.downgrade());
|
||||
cx.notify();
|
||||
});
|
||||
workspace
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,19 +8,20 @@ use call::ActiveCall;
|
||||
use client::RECEIVE_TIMEOUT;
|
||||
use collections::BTreeMap;
|
||||
use editor::Bias;
|
||||
use fs::{FakeFs, Fs as _};
|
||||
use fs::{repository::GitFileStatus, FakeFs, Fs as _};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{executor::Deterministic, ModelHandle, Task, TestAppContext};
|
||||
use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf16};
|
||||
use lsp::FakeLanguageServer;
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{search::SearchQuery, Project, ProjectPath};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
prelude::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
env,
|
||||
ops::Range,
|
||||
@@ -148,8 +149,9 @@ async fn test_random_collaboration(
|
||||
|
||||
for (client, mut cx) in clients {
|
||||
cx.update(|cx| {
|
||||
let store = cx.remove_global::<SettingsStore>();
|
||||
cx.clear_globals();
|
||||
cx.set_global(Settings::test(cx));
|
||||
cx.set_global(store);
|
||||
drop(client);
|
||||
});
|
||||
}
|
||||
@@ -716,7 +718,10 @@ async fn apply_client_operation(
|
||||
);
|
||||
|
||||
let search = project.update(cx, |project, cx| {
|
||||
project.search(SearchQuery::text(query, false, false), cx)
|
||||
project.search(
|
||||
SearchQuery::text(query, false, false, Vec::new(), Vec::new()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
drop(project);
|
||||
let search = cx.background().spawn(async move {
|
||||
@@ -760,31 +765,85 @@ async fn apply_client_operation(
|
||||
}
|
||||
}
|
||||
|
||||
ClientOperation::WriteGitIndex {
|
||||
repo_path,
|
||||
contents,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git index for repo {:?}: {:?}",
|
||||
client.username,
|
||||
ClientOperation::GitOperation { operation } => match operation {
|
||||
GitOperation::WriteGitIndex {
|
||||
repo_path,
|
||||
contents
|
||||
);
|
||||
contents,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
let contents = contents
|
||||
.iter()
|
||||
.map(|(path, contents)| (path.as_path(), contents.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
log::info!(
|
||||
"{}: writing git index for repo {:?}: {:?}",
|
||||
client.username,
|
||||
repo_path,
|
||||
contents
|
||||
);
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
let contents = contents
|
||||
.iter()
|
||||
.map(|(path, contents)| (path.as_path(), contents.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
|
||||
}
|
||||
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
|
||||
}
|
||||
GitOperation::WriteGitBranch {
|
||||
repo_path,
|
||||
new_branch,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git branch for repo {:?}: {:?}",
|
||||
client.username,
|
||||
repo_path,
|
||||
new_branch
|
||||
);
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
client.fs.set_branch_name(&dot_git_dir, new_branch).await;
|
||||
}
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
} => {
|
||||
if !client.fs.directories().contains(&repo_path) {
|
||||
return Err(TestError::Inapplicable);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"{}: writing git statuses for repo {:?}: {:?}",
|
||||
client.username,
|
||||
repo_path,
|
||||
statuses
|
||||
);
|
||||
|
||||
let dot_git_dir = repo_path.join(".git");
|
||||
|
||||
let statuses = statuses
|
||||
.iter()
|
||||
.map(|(path, val)| (path.as_path(), val.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||
client.fs.create_dir(&dot_git_dir).await?;
|
||||
}
|
||||
|
||||
client
|
||||
.fs
|
||||
.set_status_for_repo(&dot_git_dir, statuses.as_slice())
|
||||
.await;
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -859,6 +918,12 @@ fn check_consistency_between_clients(clients: &[(Rc<TestClient>, TestAppContext)
|
||||
host_snapshot.abs_path(),
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.repositories().collect::<Vec<_>>(), host_snapshot.repositories().collect::<Vec<_>>(),
|
||||
"{} has different repositories than the host for worktree {:?} and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id(),
|
||||
"{} has different scan id than the host for worktree {:?} and project {:?}",
|
||||
client.username,
|
||||
@@ -1147,10 +1212,25 @@ enum ClientOperation {
|
||||
is_dir: bool,
|
||||
content: String,
|
||||
},
|
||||
GitOperation {
|
||||
operation: GitOperation,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum GitOperation {
|
||||
WriteGitIndex {
|
||||
repo_path: PathBuf,
|
||||
contents: Vec<(PathBuf, String)>,
|
||||
},
|
||||
WriteGitBranch {
|
||||
repo_path: PathBuf,
|
||||
new_branch: Option<String>,
|
||||
},
|
||||
WriteGitStatuses {
|
||||
repo_path: PathBuf,
|
||||
statuses: Vec<(PathBuf, GitFileStatus)>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -1663,38 +1743,10 @@ impl TestPlan {
|
||||
}
|
||||
}
|
||||
|
||||
// Update a git index
|
||||
// Update a git related action
|
||||
91..=95 => {
|
||||
let repo_path = client
|
||||
.fs
|
||||
.directories()
|
||||
.choose(&mut self.rng)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
let mut file_paths = client
|
||||
.fs
|
||||
.files()
|
||||
.into_iter()
|
||||
.filter(|path| path.starts_with(&repo_path))
|
||||
.collect::<Vec<_>>();
|
||||
let count = self.rng.gen_range(0..=file_paths.len());
|
||||
file_paths.shuffle(&mut self.rng);
|
||||
file_paths.truncate(count);
|
||||
|
||||
let mut contents = Vec::new();
|
||||
for abs_child_file_path in &file_paths {
|
||||
let child_file_path = abs_child_file_path
|
||||
.strip_prefix(&repo_path)
|
||||
.unwrap()
|
||||
.to_path_buf();
|
||||
let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
|
||||
contents.push((child_file_path, new_base));
|
||||
}
|
||||
|
||||
break ClientOperation::WriteGitIndex {
|
||||
repo_path,
|
||||
contents,
|
||||
break ClientOperation::GitOperation {
|
||||
operation: self.generate_git_operation(client),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1732,6 +1784,86 @@ impl TestPlan {
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_git_operation(&mut self, client: &TestClient) -> GitOperation {
|
||||
fn generate_file_paths(
|
||||
repo_path: &Path,
|
||||
rng: &mut StdRng,
|
||||
client: &TestClient,
|
||||
) -> Vec<PathBuf> {
|
||||
let mut paths = client
|
||||
.fs
|
||||
.files()
|
||||
.into_iter()
|
||||
.filter(|path| path.starts_with(repo_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let count = rng.gen_range(0..=paths.len());
|
||||
paths.shuffle(rng);
|
||||
paths.truncate(count);
|
||||
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| path.strip_prefix(repo_path).unwrap().to_path_buf())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
let repo_path = client
|
||||
.fs
|
||||
.directories()
|
||||
.choose(&mut self.rng)
|
||||
.unwrap()
|
||||
.clone();
|
||||
|
||||
match self.rng.gen_range(0..100_u32) {
|
||||
0..=25 => {
|
||||
let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
|
||||
|
||||
let contents = file_paths
|
||||
.into_iter()
|
||||
.map(|path| (path, Alphanumeric.sample_string(&mut self.rng, 16)))
|
||||
.collect();
|
||||
|
||||
GitOperation::WriteGitIndex {
|
||||
repo_path,
|
||||
contents,
|
||||
}
|
||||
}
|
||||
26..=63 => {
|
||||
let new_branch = (self.rng.gen_range(0..10) > 3)
|
||||
.then(|| Alphanumeric.sample_string(&mut self.rng, 8));
|
||||
|
||||
GitOperation::WriteGitBranch {
|
||||
repo_path,
|
||||
new_branch,
|
||||
}
|
||||
}
|
||||
64..=100 => {
|
||||
let file_paths = generate_file_paths(&repo_path, &mut self.rng, client);
|
||||
|
||||
let statuses = file_paths
|
||||
.into_iter()
|
||||
.map(|paths| {
|
||||
(
|
||||
paths,
|
||||
match self.rng.gen_range(0..3_u32) {
|
||||
0 => GitFileStatus::Added,
|
||||
1 => GitFileStatus::Modified,
|
||||
2 => GitFileStatus::Conflict,
|
||||
_ => unreachable!(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
GitOperation::WriteGitStatuses {
|
||||
repo_path,
|
||||
statuses,
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn next_root_dir_name(&mut self, user_id: UserId) -> String {
|
||||
let user_ix = self
|
||||
.users
|
||||
|
||||
@@ -39,12 +39,13 @@ settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
log = "0.4"
|
||||
postage = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
log.workspace = true
|
||||
postage.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
call = { path = "../call", features = ["test-support"] }
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use crate::{
|
||||
collaborator_list_popover, collaborator_list_popover::CollaboratorListPopover,
|
||||
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
|
||||
ToggleScreenSharing,
|
||||
toggle_screen_sharing, ToggleScreenSharing,
|
||||
};
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use contacts_popover::ContactsPopover;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
@@ -13,22 +12,22 @@ use gpui::{
|
||||
color::Color,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f, PathBuilder},
|
||||
impl_internal_actions,
|
||||
json::{self, ToJson},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, ImageData, ModelHandle, RenderContext, Subscription, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use project::Project;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::{AvatarStyle, Theme};
|
||||
use util::ResultExt;
|
||||
use workspace::{FollowNextCollaborator, JoinProject, ToggleFollow, Workspace};
|
||||
use workspace::{FollowNextCollaborator, Workspace};
|
||||
|
||||
const MAX_TITLE_LENGTH: usize = 75;
|
||||
|
||||
actions!(
|
||||
collab,
|
||||
[
|
||||
ToggleCollaboratorList,
|
||||
ToggleContactsMenu,
|
||||
ToggleUserMenu,
|
||||
ShareProject,
|
||||
@@ -36,26 +35,20 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
impl_internal_actions!(collab, [LeaveCall]);
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub(crate) struct LeaveCall;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(CollabTitlebarItem::toggle_collaborator_list_popover);
|
||||
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||
cx.add_action(CollabTitlebarItem::share_project);
|
||||
cx.add_action(CollabTitlebarItem::unshare_project);
|
||||
cx.add_action(CollabTitlebarItem::leave_call);
|
||||
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
||||
}
|
||||
|
||||
pub struct CollabTitlebarItem {
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
client: Arc<Client>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||
user_menu: ViewHandle<ContextMenu>,
|
||||
collaborator_list_popover: Option<ViewHandle<CollaboratorListPopover>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -68,41 +61,22 @@ impl View for CollabTitlebarItem {
|
||||
"CollabTitlebarItem"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let workspace = if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
workspace
|
||||
} else {
|
||||
return Empty::new().boxed();
|
||||
return Empty::new().into_any();
|
||||
};
|
||||
|
||||
let project = workspace.read(cx).project().read(cx);
|
||||
let mut project_title = String::new();
|
||||
for (i, name) in project.worktree_root_names(cx).enumerate() {
|
||||
if i > 0 {
|
||||
project_title.push_str(", ");
|
||||
}
|
||||
project_title.push_str(name);
|
||||
}
|
||||
if project_title.is_empty() {
|
||||
project_title = "empty project".to_owned();
|
||||
}
|
||||
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
|
||||
let project = self.project.read(cx);
|
||||
let theme = theme::current(cx).clone();
|
||||
let mut left_container = Flex::row();
|
||||
let mut right_container = Flex::row().align_children_center();
|
||||
|
||||
left_container.add_child(
|
||||
Label::new(project_title, theme.workspace.titlebar.title.clone())
|
||||
.contained()
|
||||
.with_margin_right(theme.workspace.titlebar.item_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed(),
|
||||
);
|
||||
left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
|
||||
|
||||
let user = workspace.read(cx).user_store().read(cx).current_user();
|
||||
let peer_id = workspace.read(cx).client().peer_id();
|
||||
let user = self.user_store.read(cx).current_user();
|
||||
let peer_id = self.client.peer_id();
|
||||
if let Some(((user, peer_id), room)) = user
|
||||
.zip(peer_id)
|
||||
.zip(ActiveCall::global(cx).read(cx).room().cloned())
|
||||
@@ -128,28 +102,46 @@ impl View for CollabTitlebarItem {
|
||||
}
|
||||
|
||||
Stack::new()
|
||||
.with_child(left_container.boxed())
|
||||
.with_child(right_container.aligned().right().boxed())
|
||||
.boxed()
|
||||
.with_child(left_container)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
right_container.contained().with_background_color(
|
||||
theme
|
||||
.workspace
|
||||
.titlebar
|
||||
.container
|
||||
.background_color
|
||||
.unwrap_or_else(|| Color::transparent_black()),
|
||||
),
|
||||
)
|
||||
.aligned()
|
||||
.right(),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl CollabTitlebarItem {
|
||||
pub fn new(
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
workspace: &Workspace,
|
||||
workspace_handle: &ViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let project = workspace.project().clone();
|
||||
let user_store = workspace.app_state().user_store.clone();
|
||||
let client = workspace.app_state().client.clone();
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let mut subscriptions = Vec::new();
|
||||
subscriptions.push(cx.observe(workspace, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(workspace_handle, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&project, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx)));
|
||||
subscriptions.push(cx.observe_window_activation(|this, active, cx| {
|
||||
this.window_activation_changed(active, cx)
|
||||
}));
|
||||
subscriptions.push(cx.observe(user_store, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(
|
||||
cx.subscribe(user_store, move |this, user_store, event, cx| {
|
||||
cx.subscribe(&user_store, move |this, user_store, event, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let client::Event::Contact { user, kind } = event {
|
||||
@@ -171,31 +163,88 @@ impl CollabTitlebarItem {
|
||||
}),
|
||||
);
|
||||
|
||||
let view_id = cx.view_id();
|
||||
Self {
|
||||
workspace: workspace.downgrade(),
|
||||
user_store: user_store.clone(),
|
||||
workspace: workspace.weak_handle(),
|
||||
project,
|
||||
user_store,
|
||||
client,
|
||||
contacts_popover: None,
|
||||
user_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
let mut menu = ContextMenu::new(view_id, cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
collaborator_list_popover: None,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project = if active {
|
||||
Some(workspace.read(cx).project().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
|
||||
.detach_and_log_err(cx);
|
||||
fn collect_title_root_names(
|
||||
&self,
|
||||
project: &Project,
|
||||
theme: Arc<Theme>,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let names_and_branches = project.visible_worktrees(cx).map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
(worktree.root_name(), worktree.root_git_entry())
|
||||
});
|
||||
|
||||
fn push_str(buffer: &mut String, index: &mut usize, str: &str) {
|
||||
buffer.push_str(str);
|
||||
*index += str.chars().count();
|
||||
}
|
||||
|
||||
let mut indices = Vec::new();
|
||||
let mut index = 0;
|
||||
let mut title = String::new();
|
||||
let mut names_and_branches = names_and_branches.peekable();
|
||||
while let Some((name, entry)) = names_and_branches.next() {
|
||||
let pre_index = index;
|
||||
push_str(&mut title, &mut index, name);
|
||||
indices.extend((pre_index..index).into_iter());
|
||||
if let Some(branch) = entry.and_then(|entry| entry.branch()) {
|
||||
push_str(&mut title, &mut index, "/");
|
||||
push_str(&mut title, &mut index, &branch);
|
||||
}
|
||||
if names_and_branches.peek().is_some() {
|
||||
push_str(&mut title, &mut index, ", ");
|
||||
if index >= MAX_TITLE_LENGTH {
|
||||
title.push_str(" …");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let text_style = theme.workspace.titlebar.title.clone();
|
||||
let item_spacing = theme.workspace.titlebar.item_spacing;
|
||||
|
||||
let mut highlight = text_style.clone();
|
||||
highlight.color = theme.workspace.titlebar.highlight_color;
|
||||
|
||||
let style = LabelStyle {
|
||||
text: text_style,
|
||||
highlight_text: Some(highlight),
|
||||
};
|
||||
|
||||
Label::new(title, style)
|
||||
.with_highlights(indices)
|
||||
.contained()
|
||||
.with_margin_right(item_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.into_any_named("title-with-git-information")
|
||||
}
|
||||
|
||||
fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
let project = if active {
|
||||
Some(self.project.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.set_location(project.as_ref(), cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@@ -206,80 +255,49 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
|
||||
fn share_project(&mut self, _: &ShareProject, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = workspace.read(cx).project().clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.share_project(project, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = self.project.clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.share_project(project, cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn unshare_project(&mut self, _: &UnshareProject, cx: &mut ViewContext<Self>) {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = workspace.read(cx).project().clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.unshare_project(project, cx))
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_collaborator_list_popover(
|
||||
&mut self,
|
||||
_: &ToggleCollaboratorList,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match self.collaborator_list_popover.take() {
|
||||
Some(_) => {}
|
||||
None => {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let user_store = workspace.read(cx).user_store().clone();
|
||||
let view = cx.add_view(|cx| CollaboratorListPopover::new(user_store, cx));
|
||||
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
collaborator_list_popover::Event::Dismissed => {
|
||||
this.collaborator_list_popover = None;
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.collaborator_list_popover = Some(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let project = self.project.clone();
|
||||
active_call
|
||||
.update(cx, |call, cx| call.unshare_project(project, cx))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.contacts_popover.take().is_none() {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let user_store = workspace.read(cx).user_store().clone();
|
||||
let view = cx.add_view(|cx| ContactsPopover::new(project, user_store, cx));
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
contacts_popover::Event::Dismissed => {
|
||||
this.contacts_popover = None;
|
||||
}
|
||||
let view = cx.add_view(|cx| {
|
||||
ContactsPopover::new(
|
||||
self.project.clone(),
|
||||
self.user_store.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
contacts_popover::Event::Dismissed => {
|
||||
this.contacts_popover = None;
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.contacts_popover = Some(view);
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.contacts_popover = Some(view);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = theme::current(cx).clone();
|
||||
let avatar_style = theme.workspace.titlebar.leader_avatar.clone();
|
||||
let item_style = theme.context_menu.item.disabled_style().clone();
|
||||
self.user_menu.update(cx, |user_menu, cx| {
|
||||
@@ -294,21 +312,27 @@ impl CollabTitlebarItem {
|
||||
Color::transparent_black(),
|
||||
)
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), item_style.label.clone())
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Label::new(
|
||||
user.github_login.clone(),
|
||||
item_style.label.clone(),
|
||||
))
|
||||
.contained()
|
||||
.with_style(item_style.container)
|
||||
.boxed()
|
||||
.into_any()
|
||||
})),
|
||||
ContextMenuItem::item("Sign out", SignOut),
|
||||
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
|
||||
ContextMenuItem::action("Sign out", SignOut),
|
||||
ContextMenuItem::action(
|
||||
"Send Feedback",
|
||||
feedback::feedback_editor::GiveFeedback,
|
||||
),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
ContextMenuItem::item("Sign in", SignIn),
|
||||
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
|
||||
ContextMenuItem::action("Sign in", SignIn),
|
||||
ContextMenuItem::action(
|
||||
"Send Feedback",
|
||||
feedback::feedback_editor::GiveFeedback,
|
||||
),
|
||||
]
|
||||
};
|
||||
|
||||
@@ -316,17 +340,11 @@ impl CollabTitlebarItem {
|
||||
});
|
||||
}
|
||||
|
||||
fn leave_call(&mut self, _: &LeaveCall, cx: &mut ViewContext<Self>) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn render_toggle_contacts_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
|
||||
let badge = if self
|
||||
@@ -345,14 +363,13 @@ impl CollabTitlebarItem {
|
||||
.contained()
|
||||
.with_margin_left(titlebar.toggle_contacts_button.default.icon_width)
|
||||
.with_margin_top(titlebar.toggle_contacts_button.default.icon_width)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
};
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleContactsMenu>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_contacts_button
|
||||
.style_for(state, self.contacts_popover.is_some());
|
||||
@@ -366,32 +383,30 @@ impl CollabTitlebarItem {
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_contacts_popover(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleContactsMenu, _>(
|
||||
.with_tooltip::<ToggleContactsMenu>(
|
||||
0,
|
||||
"Show contacts menu".into(),
|
||||
Some(Box::new(ToggleContactsMenu)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
),
|
||||
)
|
||||
.with_children(badge)
|
||||
.with_children(self.render_contacts_popover_host(titlebar, cx))
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_toggle_screen_sharing_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
room: &ModelHandle<Room>,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let icon;
|
||||
let tooltip;
|
||||
if room.read(cx).is_screen_sharing() {
|
||||
@@ -403,7 +418,7 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
MouseEventHandler::<ToggleScreenSharing>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
@@ -415,13 +430,12 @@ impl CollabTitlebarItem {
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleScreenSharing);
|
||||
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||
toggle_screen_sharing(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleScreenSharing, _>(
|
||||
.with_tooltip::<ToggleScreenSharing>(
|
||||
0,
|
||||
tooltip.into(),
|
||||
Some(Box::new(ToggleScreenSharing)),
|
||||
@@ -429,15 +443,15 @@ impl CollabTitlebarItem {
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_in_call_share_unshare_button(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
let project = workspace.read(cx).project();
|
||||
if project.read(cx).is_remote() {
|
||||
return None;
|
||||
@@ -457,46 +471,46 @@ impl CollabTitlebarItem {
|
||||
Some(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ShareUnshare>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
|
||||
//TODO: Ensure this button has consistant width for both text variations
|
||||
let style = titlebar
|
||||
.share_button
|
||||
.style_for(state, self.contacts_popover.is_some());
|
||||
let style = titlebar.share_button.style_for(state, false);
|
||||
Label::new(label, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if is_shared {
|
||||
cx.dispatch_action(UnshareProject);
|
||||
this.unshare_project(&Default::default(), cx);
|
||||
} else {
|
||||
cx.dispatch_action(ShareProject);
|
||||
this.share_project(&Default::default(), cx);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<ShareUnshare, _>(
|
||||
.with_tooltip::<ShareUnshare>(
|
||||
0,
|
||||
tooltip.to_owned(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
),
|
||||
)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.boxed(),
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_user_menu_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render_user_menu_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleUserMenu>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar.call_control.style_for(state, false);
|
||||
Svg::new("icons/ellipsis_14.svg")
|
||||
.with_color(style.color)
|
||||
@@ -508,13 +522,12 @@ impl CollabTitlebarItem {
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleUserMenu);
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_user_menu(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleUserMenu, _>(
|
||||
.with_tooltip::<ToggleUserMenu>(
|
||||
0,
|
||||
"Toggle user menu".to_owned(),
|
||||
Some(Box::new(ToggleUserMenu)),
|
||||
@@ -522,49 +535,49 @@ impl CollabTitlebarItem {
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing)
|
||||
.boxed(),
|
||||
.with_margin_left(theme.workspace.titlebar.item_spacing),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.user_menu, cx)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.right()
|
||||
.boxed(),
|
||||
.right(),
|
||||
)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_sign_in_button(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let titlebar = &theme.workspace.titlebar;
|
||||
MouseEventHandler::<SignIn>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar.sign_in_prompt.style_for(state, false);
|
||||
Label::new("Sign In", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(SignIn);
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
let client = this.client.clone();
|
||||
cx.app_context()
|
||||
.spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_contacts_popover_host<'a>(
|
||||
&'a self,
|
||||
_theme: &'a theme::Titlebar,
|
||||
cx: &'a RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
cx: &'a ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
self.contacts_popover.as_ref().map(|popover| {
|
||||
Overlay::new(ChildView::new(popover, cx).boxed())
|
||||
Overlay::new(ChildView::new(popover, cx))
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::TopRight)
|
||||
.with_z_index(999)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.right()
|
||||
.boxed()
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -573,8 +586,8 @@ impl CollabTitlebarItem {
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
room: &ModelHandle<Room>,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Vec<ElementBox> {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Vec<Container<Self>> {
|
||||
let mut participants = room
|
||||
.read(cx)
|
||||
.remote_participants()
|
||||
@@ -602,8 +615,7 @@ impl CollabTitlebarItem {
|
||||
theme,
|
||||
cx,
|
||||
))
|
||||
.with_margin_right(theme.workspace.titlebar.face_pile_spacing)
|
||||
.boxed(),
|
||||
.with_margin_right(theme.workspace.titlebar.face_pile_spacing),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
@@ -615,8 +627,8 @@ impl CollabTitlebarItem {
|
||||
theme: &Theme,
|
||||
user: &Arc<User>,
|
||||
peer_id: PeerId,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let replica_id = workspace.read(cx).project().read(cx).replica_id();
|
||||
Container::new(self.render_face_pile(
|
||||
user,
|
||||
@@ -628,7 +640,7 @@ impl CollabTitlebarItem {
|
||||
cx,
|
||||
))
|
||||
.with_margin_right(theme.workspace.titlebar.item_spacing)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_face_pile(
|
||||
@@ -639,8 +651,8 @@ impl CollabTitlebarItem {
|
||||
location: Option<ParticipantLocation>,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
theme: &Theme,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let project_id = workspace.read(cx).project().read(cx).remote_id();
|
||||
let room = ActiveCall::global(cx).read(cx).room();
|
||||
let is_being_followed = workspace.read(cx).is_being_followed(peer_id);
|
||||
@@ -710,11 +722,9 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
})?;
|
||||
|
||||
let location = remote_participant.map(|p| p.location);
|
||||
|
||||
Some(Self::render_face(
|
||||
avatar.clone(),
|
||||
Self::location_style(workspace, location, follower_style, cx),
|
||||
follower_style,
|
||||
background_color,
|
||||
))
|
||||
}))
|
||||
@@ -734,7 +744,7 @@ impl CollabTitlebarItem {
|
||||
}
|
||||
}
|
||||
|
||||
container.boxed()
|
||||
container
|
||||
}))
|
||||
.with_children((|| {
|
||||
let replica_id = replica_id?;
|
||||
@@ -745,56 +755,67 @@ impl CollabTitlebarItem {
|
||||
.with_width(theme.workspace.titlebar.avatar_ribbon.width)
|
||||
.with_height(theme.workspace.titlebar.avatar_ribbon.height)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.boxed(),
|
||||
.bottom(),
|
||||
)
|
||||
})())
|
||||
.boxed();
|
||||
.into_any();
|
||||
|
||||
if let Some(location) = location {
|
||||
if let Some(replica_id) = replica_id {
|
||||
content =
|
||||
MouseEventHandler::<ToggleFollow>::new(replica_id.into(), cx, move |_, _| {
|
||||
content
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleFollow(peer_id))
|
||||
})
|
||||
.with_tooltip::<ToggleFollow, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
if is_being_followed {
|
||||
format!("Unfollow {}", user.github_login)
|
||||
} else {
|
||||
format!("Follow {}", user.github_login)
|
||||
},
|
||||
Some(Box::new(FollowNextCollaborator)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed();
|
||||
enum ToggleFollow {}
|
||||
|
||||
content = MouseEventHandler::<ToggleFollow, Self>::new(
|
||||
replica_id.into(),
|
||||
cx,
|
||||
move |_, _| content,
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, item, cx| {
|
||||
if let Some(workspace) = item.workspace.upgrade(cx) {
|
||||
if let Some(task) = workspace
|
||||
.update(cx, |workspace, cx| workspace.toggle_follow(peer_id, cx))
|
||||
{
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.with_tooltip::<ToggleFollow>(
|
||||
peer_id.as_u64() as usize,
|
||||
if is_being_followed {
|
||||
format!("Unfollow {}", user.github_login)
|
||||
} else {
|
||||
format!("Follow {}", user.github_login)
|
||||
},
|
||||
Some(Box::new(FollowNextCollaborator)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any();
|
||||
} else if let ParticipantLocation::SharedProject { project_id } = location {
|
||||
enum JoinProject {}
|
||||
|
||||
let user_id = user.id;
|
||||
content = MouseEventHandler::<JoinProject>::new(
|
||||
content = MouseEventHandler::<JoinProject, Self>::new(
|
||||
peer_id.as_u64() as usize,
|
||||
cx,
|
||||
move |_, _| content,
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(JoinProject {
|
||||
project_id,
|
||||
follow_user_id: user_id,
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
let app_state = workspace.read(cx).app_state().clone();
|
||||
workspace::join_remote_project(project_id, user_id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<JoinProject, _>(
|
||||
.with_tooltip::<JoinProject>(
|
||||
peer_id.as_u64() as usize,
|
||||
format!("Follow {} into external project", user.github_login),
|
||||
Some(Box::new(FollowNextCollaborator)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed();
|
||||
.into_any();
|
||||
}
|
||||
}
|
||||
content
|
||||
@@ -804,7 +825,7 @@ impl CollabTitlebarItem {
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
location: Option<ParticipantLocation>,
|
||||
mut style: AvatarStyle,
|
||||
cx: &RenderContext<Self>,
|
||||
cx: &ViewContext<Self>,
|
||||
) -> AvatarStyle {
|
||||
if let Some(location) = location {
|
||||
if let ParticipantLocation::SharedProject { project_id } = location {
|
||||
@@ -819,11 +840,11 @@ impl CollabTitlebarItem {
|
||||
style
|
||||
}
|
||||
|
||||
fn render_face(
|
||||
fn render_face<V: View>(
|
||||
avatar: Arc<ImageData>,
|
||||
avatar_style: AvatarStyle,
|
||||
background_color: Color,
|
||||
) -> ElementBox {
|
||||
) -> AnyElement<V> {
|
||||
Image::from_data(avatar)
|
||||
.with_style(avatar_style.image)
|
||||
.aligned()
|
||||
@@ -834,40 +855,34 @@ impl CollabTitlebarItem {
|
||||
.with_width(avatar_style.outer_width)
|
||||
.with_height(avatar_style.outer_width)
|
||||
.aligned()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_connection_status(
|
||||
&self,
|
||||
status: &client::Status,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> Option<ElementBox> {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
enum ConnectionStatusButton {}
|
||||
|
||||
let theme = &cx.global::<Settings>().theme.clone();
|
||||
let theme = &theme::current(cx).clone();
|
||||
match status {
|
||||
client::Status::ConnectionError
|
||||
| client::Status::ConnectionLost
|
||||
| client::Status::Reauthenticating { .. }
|
||||
| client::Status::Reconnecting { .. }
|
||||
| client::Status::ReconnectionError { .. } => Some(
|
||||
Container::new(
|
||||
Align::new(
|
||||
ConstrainedBox::new(
|
||||
Svg::new("icons/cloud_slash_12.svg")
|
||||
.with_color(theme.workspace.titlebar.offline_icon.color)
|
||||
.boxed(),
|
||||
)
|
||||
.with_width(theme.workspace.titlebar.offline_icon.width)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(theme.workspace.titlebar.offline_icon.container)
|
||||
.boxed(),
|
||||
Svg::new("icons/cloud_slash_12.svg")
|
||||
.with_color(theme.workspace.titlebar.offline_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.workspace.titlebar.offline_icon.width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.workspace.titlebar.offline_icon.container)
|
||||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => Some(
|
||||
MouseEventHandler::<ConnectionStatusButton>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
|
||||
Label::new(
|
||||
"Please update Zed to collaborate",
|
||||
theme.workspace.titlebar.outdated_warning.text.clone(),
|
||||
@@ -875,13 +890,12 @@ impl CollabTitlebarItem {
|
||||
.contained()
|
||||
.with_style(theme.workspace.titlebar.outdated_warning.container)
|
||||
.aligned()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(auto_update::Check);
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
auto_update::check(&Default::default(), cx);
|
||||
})
|
||||
.boxed(),
|
||||
.into_any(),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
@@ -898,7 +912,7 @@ impl AvatarRibbon {
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for AvatarRibbon {
|
||||
impl Element<CollabTitlebarItem> for AvatarRibbon {
|
||||
type LayoutState = ();
|
||||
|
||||
type PaintState = ();
|
||||
@@ -906,17 +920,20 @@ impl Element for AvatarRibbon {
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
_: &mut gpui::LayoutContext,
|
||||
_: &mut CollabTitlebarItem,
|
||||
_: &mut LayoutContext<CollabTitlebarItem>,
|
||||
) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) {
|
||||
(constraint.max, ())
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
bounds: gpui::geometry::rect::RectF,
|
||||
_: gpui::geometry::rect::RectF,
|
||||
scene: &mut SceneBuilder,
|
||||
bounds: RectF,
|
||||
_: RectF,
|
||||
_: &mut Self::LayoutState,
|
||||
cx: &mut gpui::PaintContext,
|
||||
_: &mut CollabTitlebarItem,
|
||||
_: &mut ViewContext<CollabTitlebarItem>,
|
||||
) -> Self::PaintState {
|
||||
let mut path = PathBuilder::new();
|
||||
path.reset(bounds.lower_left());
|
||||
@@ -927,7 +944,7 @@ impl Element for AvatarRibbon {
|
||||
path.line_to(bounds.upper_right() - vec2f(bounds.height(), 0.));
|
||||
path.curve_to(bounds.lower_right(), bounds.upper_right());
|
||||
path.line_to(bounds.lower_left());
|
||||
cx.scene.push_path(path.build(self.color, None));
|
||||
scene.push_path(path.build(self.color, None));
|
||||
}
|
||||
|
||||
fn rect_for_text_range(
|
||||
@@ -937,17 +954,19 @@ impl Element for AvatarRibbon {
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &gpui::MeasurementContext,
|
||||
_: &CollabTitlebarItem,
|
||||
_: &ViewContext<CollabTitlebarItem>,
|
||||
) -> Option<RectF> {
|
||||
None
|
||||
}
|
||||
|
||||
fn debug(
|
||||
&self,
|
||||
bounds: gpui::geometry::rect::RectF,
|
||||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &gpui::DebugContext,
|
||||
_: &CollabTitlebarItem,
|
||||
_: &ViewContext<CollabTitlebarItem>,
|
||||
) -> gpui::json::Value {
|
||||
json::json!({
|
||||
"type": "AvatarRibbon",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
mod collab_titlebar_item;
|
||||
mod collaborator_list_popover;
|
||||
mod contact_finder;
|
||||
mod contact_list;
|
||||
mod contact_notification;
|
||||
@@ -10,29 +9,24 @@ mod notifications;
|
||||
mod project_shared_notification;
|
||||
mod sharing_status_indicator;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use call::ActiveCall;
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use gpui::{actions, AppContext, Task};
|
||||
use std::sync::Arc;
|
||||
use workspace::{AppState, JoinProject, ToggleFollow, Workspace};
|
||||
use workspace::AppState;
|
||||
|
||||
actions!(collab, [ToggleScreenSharing]);
|
||||
|
||||
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
collab_titlebar_item::init(cx);
|
||||
contact_notification::init(cx);
|
||||
contact_list::init(cx);
|
||||
contact_finder::init(cx);
|
||||
contacts_popover::init(cx);
|
||||
incoming_call_notification::init(cx);
|
||||
project_shared_notification::init(cx);
|
||||
incoming_call_notification::init(&app_state, cx);
|
||||
project_shared_notification::init(&app_state, cx);
|
||||
sharing_status_indicator::init(cx);
|
||||
|
||||
cx.add_global_action(toggle_screen_sharing);
|
||||
cx.add_global_action(move |action: &JoinProject, cx| {
|
||||
join_project(action, app_state.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
@@ -47,88 +41,3 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||
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(window_id)?.clone().downcast::<Workspace>())
|
||||
.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)(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
|
||||
};
|
||||
|
||||
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)
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
use call::ActiveCall;
|
||||
use client::UserStore;
|
||||
use gpui::Action;
|
||||
use gpui::{
|
||||
actions, elements::*, platform::MouseButton, Entity, ModelHandle, 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,49 +1,41 @@
|
||||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{
|
||||
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, RenderContext, Task,
|
||||
View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ContactFinder>::init(cx);
|
||||
Picker::<ContactFinderDelegate>::init(cx);
|
||||
}
|
||||
|
||||
pub struct ContactFinder {
|
||||
picker: ViewHandle<Picker<Self>>,
|
||||
pub type ContactFinder = Picker<ContactFinderDelegate>;
|
||||
|
||||
pub fn build_contact_finder(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ViewContext<ContactFinder>,
|
||||
) -> ContactFinder {
|
||||
Picker::new(
|
||||
ContactFinderDelegate {
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.contact_finder.picker.clone())
|
||||
}
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
potential_contacts: Arc<[Arc<User>]>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl Entity for ContactFinder {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ContactFinder {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactFinder"
|
||||
impl PickerDelegate for ContactFinderDelegate {
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
ChildView::new(&self.picker, cx).boxed()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ContactFinder {
|
||||
fn match_count(&self) -> usize {
|
||||
self.potential_contacts.len()
|
||||
}
|
||||
@@ -52,22 +44,22 @@ impl PickerDelegate for ContactFinder {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
let search_users = self
|
||||
.user_store
|
||||
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let potential_contacts = search_users.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.potential_contacts = potential_contacts.into();
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
picker.delegate_mut().potential_contacts = potential_contacts.into();
|
||||
cx.notify();
|
||||
});
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
@@ -75,7 +67,7 @@ impl PickerDelegate for ContactFinder {
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(user) = self.potential_contacts.get(self.selected_index) {
|
||||
let user_store = self.user_store.read(cx);
|
||||
match user_store.contact_request_status(user) {
|
||||
@@ -94,8 +86,8 @@ impl PickerDelegate for ContactFinder {
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed);
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
@@ -104,8 +96,8 @@ impl PickerDelegate for ContactFinder {
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme;
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let theme = &theme::current(cx);
|
||||
let user = &self.potential_contacts[ix];
|
||||
let request_status = self.user_store.read(cx).contact_request_status(user);
|
||||
|
||||
@@ -132,15 +124,13 @@ impl PickerDelegate for ContactFinder {
|
||||
.with_style(theme.contact_finder.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_finder.contact_username)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed(),
|
||||
.left(),
|
||||
)
|
||||
.with_children(icon_path.map(|icon_path| {
|
||||
Svg::new(icon_path)
|
||||
@@ -155,37 +145,11 @@ impl PickerDelegate for ContactFinder {
|
||||
.with_height(button_style.button_width)
|
||||
.aligned()
|
||||
.flex_float()
|
||||
.boxed()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.contact_finder.row_height)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactFinder {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let this = cx.weak_handle();
|
||||
Self {
|
||||
picker: cx.add_view(|cx| {
|
||||
Picker::new("Search collaborator by username...", this, cx)
|
||||
.with_theme(|theme| theme.contact_finder.picker.clone())
|
||||
}),
|
||||
potential_contacts: Arc::from([]),
|
||||
user_store,
|
||||
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
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use super::collab_titlebar_item::LeaveCall;
|
||||
use crate::contacts_popover;
|
||||
use call::ActiveCall;
|
||||
use client::{proto::PeerId, Contact, User, UserStore};
|
||||
use editor::{Cancel, Editor};
|
||||
@@ -8,21 +6,19 @@ use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
impl_actions, impl_internal_actions,
|
||||
impl_actions,
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||
AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::Project;
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{mem, sync::Arc};
|
||||
use theme::IconButton;
|
||||
use workspace::{JoinProject, OpenSharedScreen};
|
||||
use workspace::Workspace;
|
||||
|
||||
impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]);
|
||||
impl_internal_actions!(contact_list, [ToggleExpanded, Call]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ContactList::remove_contact);
|
||||
@@ -31,17 +27,6 @@ pub fn init(cx: &mut AppContext) {
|
||||
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);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct ToggleExpanded(Section);
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Call {
|
||||
recipient_user_id: u64,
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
||||
@@ -153,14 +138,16 @@ pub struct RespondToContactRequest {
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
ToggleContactFinder,
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
pub struct ContactList {
|
||||
entries: Vec<ContactEntry>,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
list_state: ListState,
|
||||
list_state: ListState<Self>,
|
||||
project: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
filter_editor: ViewHandle<Editor>,
|
||||
collapsed_sections: Vec<Section>,
|
||||
@@ -172,6 +159,7 @@ impl ContactList {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let filter_editor = cx.add_view(|cx| {
|
||||
@@ -202,8 +190,8 @@ impl ContactList {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let list_state = ListState::new(0, Orientation::Top, 1000., cx, move |this, ix, cx| {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let list_state = ListState::<Self>::new(0, Orientation::Top, 1000., move |this, ix, cx| {
|
||||
let theme = theme::current(cx).clone();
|
||||
let is_selected = this.selection == Some(ix);
|
||||
let current_project_id = this.project.read(cx).remote_id();
|
||||
|
||||
@@ -291,6 +279,7 @@ impl ContactList {
|
||||
filter_editor,
|
||||
_subscriptions: subscriptions,
|
||||
project,
|
||||
workspace,
|
||||
user_store,
|
||||
};
|
||||
this.update_entries(cx);
|
||||
@@ -403,18 +392,11 @@ impl ContactList {
|
||||
if let Some(entry) = self.entries.get(selection) {
|
||||
match entry {
|
||||
ContactEntry::Header(section) => {
|
||||
let section = *section;
|
||||
self.toggle_expanded(&ToggleExpanded(section), cx);
|
||||
self.toggle_expanded(*section, cx);
|
||||
}
|
||||
ContactEntry::Contact { contact, calling } => {
|
||||
if contact.online && !contact.busy && !calling {
|
||||
self.call(
|
||||
&Call {
|
||||
recipient_user_id: contact.user.id,
|
||||
initial_project: Some(self.project.clone()),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
self.call(contact.user.id, Some(self.project.clone()), cx);
|
||||
}
|
||||
}
|
||||
ContactEntry::ParticipantProject {
|
||||
@@ -422,13 +404,23 @@ impl ContactList {
|
||||
host_user_id,
|
||||
..
|
||||
} => {
|
||||
cx.dispatch_global_action(JoinProject {
|
||||
project_id: *project_id,
|
||||
follow_user_id: *host_user_id,
|
||||
});
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let app_state = workspace.read(cx).app_state().clone();
|
||||
workspace::join_remote_project(
|
||||
*project_id,
|
||||
*host_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
ContactEntry::ParticipantScreen { peer_id, .. } => {
|
||||
cx.dispatch_action(OpenSharedScreen { peer_id: *peer_id });
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(*peer_id, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -436,8 +428,7 @@ impl ContactList {
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
|
||||
let section = action.0;
|
||||
fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) {
|
||||
self.collapsed_sections.remove(ix);
|
||||
} else {
|
||||
@@ -748,14 +739,13 @@ impl ContactList {
|
||||
is_pending: bool,
|
||||
is_selected: bool,
|
||||
theme: &theme::ContactList,
|
||||
) -> ElementBox {
|
||||
) -> AnyElement<Self> {
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
@@ -766,16 +756,14 @@ impl ContactList {
|
||||
.with_style(theme.contact_username.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children(if is_pending {
|
||||
Some(
|
||||
Label::new("Calling", theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.calling_indicator.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -788,7 +776,7 @@ impl ContactList {
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_participant_project(
|
||||
@@ -799,8 +787,10 @@ impl ContactList {
|
||||
is_last: bool,
|
||||
is_selected: bool,
|
||||
theme: &theme::ContactList,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum JoinProject {}
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
let host_avatar_height = theme
|
||||
.contact_avatar
|
||||
@@ -819,48 +809,44 @@ impl ContactList {
|
||||
worktree_root_names.join(", ")
|
||||
};
|
||||
|
||||
MouseEventHandler::<JoinProject>::new(project_id as usize, cx, |mouse_state, _| {
|
||||
MouseEventHandler::<JoinProject, Self>::new(project_id as usize, cx, |mouse_state, _| {
|
||||
let tree_branch = *tree_branch.style_for(mouse_state, is_selected);
|
||||
let row = theme.project_row.style_for(mouse_state, is_selected);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Canvas::new(move |bounds, _, cx| {
|
||||
let start_x = bounds.min_x() + (bounds.width() / 2.)
|
||||
- (tree_branch.width / 2.);
|
||||
let end_x = bounds.max_x();
|
||||
let start_y = bounds.min_y();
|
||||
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
|
||||
.with_child(Canvas::new(move |scene, bounds, _, _, _| {
|
||||
let start_x =
|
||||
bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.);
|
||||
let end_x = bounds.max_x();
|
||||
let start_y = bounds.min_y();
|
||||
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
|
||||
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, start_y),
|
||||
vec2f(
|
||||
start_x + tree_branch.width,
|
||||
if is_last { end_y } else { bounds.max_y() },
|
||||
),
|
||||
scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, start_y),
|
||||
vec2f(
|
||||
start_x + tree_branch.width,
|
||||
if is_last { end_y } else { bounds.max_y() },
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, end_y),
|
||||
vec2f(end_x, end_y + tree_branch.width),
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, end_y),
|
||||
vec2f(end_x, end_y + tree_branch.width),
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
}))
|
||||
.constrained()
|
||||
.with_width(host_avatar_height)
|
||||
.boxed(),
|
||||
.with_width(host_avatar_height),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(project_name, row.name.text.clone())
|
||||
@@ -868,29 +854,28 @@ impl ContactList {
|
||||
.left()
|
||||
.contained()
|
||||
.with_style(row.name.container)
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
.flex(1., false),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(row.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(if !is_current {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::Arrow
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if !is_current {
|
||||
cx.dispatch_global_action(JoinProject {
|
||||
project_id,
|
||||
follow_user_id: host_user_id,
|
||||
});
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
let app_state = workspace.read(cx).app_state().clone();
|
||||
workspace::join_remote_project(project_id, host_user_id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_participant_screen(
|
||||
@@ -898,8 +883,10 @@ impl ContactList {
|
||||
is_last: bool,
|
||||
is_selected: bool,
|
||||
theme: &theme::ContactList,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum OpenSharedScreen {}
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
let host_avatar_height = theme
|
||||
.contact_avatar
|
||||
@@ -913,7 +900,7 @@ impl ContactList {
|
||||
let baseline_offset =
|
||||
row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.;
|
||||
|
||||
MouseEventHandler::<OpenSharedScreen>::new(
|
||||
MouseEventHandler::<OpenSharedScreen, Self>::new(
|
||||
peer_id.as_u64() as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
@@ -923,42 +910,37 @@ impl ContactList {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Canvas::new(move |bounds, _, cx| {
|
||||
let start_x = bounds.min_x() + (bounds.width() / 2.)
|
||||
- (tree_branch.width / 2.);
|
||||
let end_x = bounds.max_x();
|
||||
let start_y = bounds.min_y();
|
||||
let end_y =
|
||||
bounds.min_y() + baseline_offset - (cap_height / 2.);
|
||||
.with_child(Canvas::new(move |scene, bounds, _, _, _| {
|
||||
let start_x = bounds.min_x() + (bounds.width() / 2.)
|
||||
- (tree_branch.width / 2.);
|
||||
let end_x = bounds.max_x();
|
||||
let start_y = bounds.min_y();
|
||||
let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.);
|
||||
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, start_y),
|
||||
vec2f(
|
||||
start_x + tree_branch.width,
|
||||
if is_last { end_y } else { bounds.max_y() },
|
||||
),
|
||||
scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, start_y),
|
||||
vec2f(
|
||||
start_x + tree_branch.width,
|
||||
if is_last { end_y } else { bounds.max_y() },
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
cx.scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, end_y),
|
||||
vec2f(end_x, end_y + tree_branch.width),
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
scene.push_quad(gpui::Quad {
|
||||
bounds: RectF::from_points(
|
||||
vec2f(start_x, end_y),
|
||||
vec2f(end_x, end_y + tree_branch.width),
|
||||
),
|
||||
background: Some(tree_branch.color),
|
||||
border: gpui::Border::default(),
|
||||
corner_radius: 0.,
|
||||
});
|
||||
}))
|
||||
.constrained()
|
||||
.with_width(host_avatar_height)
|
||||
.boxed(),
|
||||
.with_width(host_avatar_height),
|
||||
)
|
||||
.with_child(
|
||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||
@@ -968,8 +950,7 @@ impl ContactList {
|
||||
.aligned()
|
||||
.left()
|
||||
.contained()
|
||||
.with_style(row.icon.container)
|
||||
.boxed(),
|
||||
.with_style(row.icon.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new("Screen", row.name.text.clone())
|
||||
@@ -977,21 +958,23 @@ impl ContactList {
|
||||
.left()
|
||||
.contained()
|
||||
.with_style(row.name.container)
|
||||
.flex(1., false)
|
||||
.boxed(),
|
||||
.flex(1., false),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(row.container)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(OpenSharedScreen { peer_id });
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_shared_screen(peer_id, cx)
|
||||
});
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
@@ -999,8 +982,8 @@ impl ContactList {
|
||||
theme: &theme::ContactList,
|
||||
is_selected: bool,
|
||||
is_collapsed: bool,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum Header {}
|
||||
enum LeaveCallContactList {}
|
||||
|
||||
@@ -1015,23 +998,25 @@ impl ContactList {
|
||||
};
|
||||
let leave_call = if section == Section::ActiveCall {
|
||||
Some(
|
||||
MouseEventHandler::<LeaveCallContactList>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::<LeaveCallContactList, Self>::new(0, cx, |state, _| {
|
||||
let style = theme.leave_call.style_for(state, false);
|
||||
Label::new("Leave Call", style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(LeaveCall))
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| call.hang_up(cx))
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let icon_size = theme.section_icon_size;
|
||||
MouseEventHandler::<Header>::new(section as usize, cx, |_, _| {
|
||||
MouseEventHandler::<Header, Self>::new(section as usize, cx, |_, _| {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new(if is_collapsed {
|
||||
@@ -1045,8 +1030,7 @@ impl ContactList {
|
||||
.with_max_height(icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(icon_size)
|
||||
.boxed(),
|
||||
.with_width(icon_size),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(text, header_style.text.clone())
|
||||
@@ -1054,21 +1038,19 @@ impl ContactList {
|
||||
.left()
|
||||
.contained()
|
||||
.with_margin_left(theme.contact_username.container.margin.left)
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_children(leave_call)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(header_style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleExpanded(section))
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_expanded(section, cx);
|
||||
})
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_contact(
|
||||
@@ -1077,15 +1059,15 @@ impl ContactList {
|
||||
project: &ModelHandle<Project>,
|
||||
theme: &theme::ContactList,
|
||||
is_selected: bool,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
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, |_, cx| {
|
||||
let mut event_handler =
|
||||
MouseEventHandler::<Contact, Self>::new(contact.user.id as usize, cx, |_, cx| {
|
||||
Flex::row()
|
||||
.with_children(contact.user.avatar.clone().map(|avatar| {
|
||||
let status_badge = if contact.online {
|
||||
@@ -1098,8 +1080,7 @@ impl ContactList {
|
||||
} else {
|
||||
theme.contact_status_free
|
||||
})
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -1109,11 +1090,9 @@ impl ContactList {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed(),
|
||||
.left(),
|
||||
)
|
||||
.with_children(status_badge)
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
@@ -1124,11 +1103,10 @@ impl ContactList {
|
||||
.with_style(theme.contact_username.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Cancel>::new(
|
||||
MouseEventHandler::<Cancel, Self>::new(
|
||||
contact.user.id as usize,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
@@ -1137,27 +1115,27 @@ impl ContactList {
|
||||
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(),
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_contact(
|
||||
&RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
.flex_float(),
|
||||
)
|
||||
.with_children(if calling {
|
||||
Some(
|
||||
Label::new("Calling", theme.calling_indicator.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.calling_indicator.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -1170,22 +1148,18 @@ impl ContactList {
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if online && !busy {
|
||||
cx.dispatch_action(Call {
|
||||
recipient_user_id: user_id,
|
||||
initial_project: Some(initial_project.clone()),
|
||||
});
|
||||
this.call(user_id, Some(initial_project.clone()), cx);
|
||||
}
|
||||
});
|
||||
|
||||
if online {
|
||||
element = element.with_cursor_style(CursorStyle::PointingHand);
|
||||
event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand);
|
||||
}
|
||||
|
||||
element.boxed()
|
||||
event_handler.into_any()
|
||||
}
|
||||
|
||||
fn render_contact_request(
|
||||
@@ -1194,8 +1168,8 @@ impl ContactList {
|
||||
theme: &theme::ContactList,
|
||||
is_incoming: bool,
|
||||
is_selected: bool,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum Decline {}
|
||||
enum Accept {}
|
||||
enum Cancel {}
|
||||
@@ -1206,7 +1180,6 @@ impl ContactList {
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
@@ -1217,8 +1190,7 @@ impl ContactList {
|
||||
.with_style(theme.contact_username.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
);
|
||||
|
||||
let user_id = user.id;
|
||||
@@ -1227,28 +1199,31 @@ impl ContactList {
|
||||
let button_spacing = theme.contact_button_spacing;
|
||||
|
||||
if is_incoming {
|
||||
row.add_children([
|
||||
MouseEventHandler::<Decline>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
row.add_child(
|
||||
MouseEventHandler::<Decline, Self>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
theme.contact_button.style_for(mouse_state, false)
|
||||
};
|
||||
render_icon_button(button_style, "icons/x_mark_8.svg")
|
||||
.aligned()
|
||||
.boxed()
|
||||
render_icon_button(button_style, "icons/x_mark_8.svg").aligned()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RespondToContactRequest {
|
||||
user_id,
|
||||
accept: false,
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.respond_to_contact_request(
|
||||
&RespondToContactRequest {
|
||||
user_id,
|
||||
accept: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.contained()
|
||||
.with_margin_right(button_spacing)
|
||||
.boxed(),
|
||||
MouseEventHandler::<Accept>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
.with_margin_right(button_spacing),
|
||||
);
|
||||
|
||||
row.add_child(
|
||||
MouseEventHandler::<Accept, Self>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
@@ -1257,20 +1232,21 @@ impl ContactList {
|
||||
render_icon_button(button_style, "icons/check_8.svg")
|
||||
.aligned()
|
||||
.flex_float()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(RespondToContactRequest {
|
||||
user_id,
|
||||
accept: true,
|
||||
})
|
||||
})
|
||||
.boxed(),
|
||||
]);
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.respond_to_contact_request(
|
||||
&RespondToContactRequest {
|
||||
user_id,
|
||||
accept: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
row.add_child(
|
||||
MouseEventHandler::<Cancel>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
MouseEventHandler::<Cancel, Self>::new(user.id as usize, cx, |mouse_state, _| {
|
||||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_button
|
||||
} else {
|
||||
@@ -1279,18 +1255,19 @@ impl ContactList {
|
||||
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(),
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.remove_contact(
|
||||
&RemoveContact {
|
||||
user_id,
|
||||
github_login: github_login.clone(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
.flex_float(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1302,12 +1279,15 @@ impl ContactList {
|
||||
.contact_row
|
||||
.style_for(&mut Default::default(), is_selected),
|
||||
)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
|
||||
let recipient_user_id = action.recipient_user_id;
|
||||
let initial_project = action.initial_project.clone();
|
||||
fn call(
|
||||
&mut self,
|
||||
recipient_user_id: u64,
|
||||
initial_project: Option<ModelHandle<Project>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
ActiveCall::global(cx)
|
||||
.update(cx, |call, cx| {
|
||||
call.invite(recipient_user_id, initial_project, cx)
|
||||
@@ -1325,15 +1305,14 @@ impl View for ContactList {
|
||||
"ContactList"
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
|
||||
Self::reset_to_default_keymap_context(keymap);
|
||||
keymap.add_identifier("menu");
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum AddContact {}
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = theme::current(cx).clone();
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
@@ -1342,36 +1321,32 @@ impl View for ContactList {
|
||||
ChildView::new(&self.filter_editor, cx)
|
||||
.contained()
|
||||
.with_style(theme.contact_list.user_query_editor.container)
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<AddContact>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::<AddContact, Self>::new(0, cx, |_, _| {
|
||||
render_icon_button(
|
||||
&theme.contact_list.add_contact_button,
|
||||
"icons/user_plus_16.svg",
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(contacts_popover::ToggleContactFinder)
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
cx.emit(Event::ToggleContactFinder)
|
||||
})
|
||||
.with_tooltip::<AddContact, _>(
|
||||
.with_tooltip::<AddContact>(
|
||||
0,
|
||||
"Search for new contact".into(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.contact_list.user_query_editor_height)
|
||||
.boxed(),
|
||||
.with_height(theme.contact_list.user_query_editor_height),
|
||||
)
|
||||
.with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
|
||||
.boxed()
|
||||
.with_child(List::new(self.list_state.clone()).flex(1., false))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
@@ -1387,7 +1362,7 @@ impl View for ContactList {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
|
||||
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<ContactList> {
|
||||
Svg::new(svg_path)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
|
||||
@@ -2,19 +2,9 @@ use std::sync::Arc;
|
||||
|
||||
use crate::notifications::render_user_notification;
|
||||
use client::{ContactEventKind, User, UserStore};
|
||||
use gpui::{
|
||||
elements::*, impl_internal_actions, AppContext, Entity, ModelHandle, RenderContext, View,
|
||||
ViewContext,
|
||||
};
|
||||
use gpui::{elements::*, Entity, ModelHandle, View, ViewContext};
|
||||
use workspace::notifications::Notification;
|
||||
|
||||
impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ContactNotification::dismiss);
|
||||
cx.add_action(ContactNotification::respond_to_contact_request);
|
||||
}
|
||||
|
||||
pub struct ContactNotification {
|
||||
user_store: ModelHandle<UserStore>,
|
||||
user: Arc<User>,
|
||||
@@ -43,26 +33,24 @@ impl View for ContactNotification {
|
||||
"ContactNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
match self.kind {
|
||||
ContactEventKind::Requested => render_user_notification(
|
||||
self.user.clone(),
|
||||
"wants to add you as a contact",
|
||||
Some("They won't be alerted if you decline."),
|
||||
Dismiss(self.user.id),
|
||||
|notification, cx| notification.dismiss(cx),
|
||||
vec![
|
||||
(
|
||||
"Decline",
|
||||
Box::new(RespondToContactRequest {
|
||||
user_id: self.user.id,
|
||||
accept: false,
|
||||
Box::new(|notification, cx| {
|
||||
notification.respond_to_contact_request(false, cx)
|
||||
}),
|
||||
),
|
||||
(
|
||||
"Accept",
|
||||
Box::new(RespondToContactRequest {
|
||||
user_id: self.user.id,
|
||||
accept: true,
|
||||
Box::new(|notification, cx| {
|
||||
notification.respond_to_contact_request(true, cx)
|
||||
}),
|
||||
),
|
||||
],
|
||||
@@ -72,7 +60,7 @@ impl View for ContactNotification {
|
||||
self.user.clone(),
|
||||
"accepted your contact request",
|
||||
None,
|
||||
Dismiss(self.user.id),
|
||||
|notification, cx| notification.dismiss(cx),
|
||||
vec![],
|
||||
cx,
|
||||
),
|
||||
@@ -114,7 +102,7 @@ impl ContactNotification {
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.user_store.update(cx, |store, cx| {
|
||||
store
|
||||
.dismiss_contact_request(self.user.id, cx)
|
||||
@@ -123,14 +111,10 @@ impl ContactNotification {
|
||||
cx.emit(Event::Dismiss);
|
||||
}
|
||||
|
||||
fn respond_to_contact_request(
|
||||
&mut self,
|
||||
action: &RespondToContactRequest,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
fn respond_to_contact_request(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
|
||||
self.user_store
|
||||
.update(cx, |store, cx| {
|
||||
store.respond_to_contact_request(action.user_id, action.accept, cx)
|
||||
store.respond_to_contact_request(self.user.id, accept, cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu};
|
||||
use crate::{
|
||||
contact_finder::{build_contact_finder, ContactFinder},
|
||||
contact_list::ContactList,
|
||||
};
|
||||
use client::UserStore;
|
||||
use gpui::{
|
||||
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, RenderContext,
|
||||
View, ViewContext, ViewHandle,
|
||||
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use picker::PickerEvent;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(contacts_popover, [ToggleContactFinder]);
|
||||
|
||||
@@ -26,6 +30,7 @@ pub struct ContactsPopover {
|
||||
child: Child,
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
_subscription: Option<gpui::Subscription>,
|
||||
}
|
||||
|
||||
@@ -33,14 +38,16 @@ impl ContactsPopover {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
child: Child::ContactList(
|
||||
cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)),
|
||||
),
|
||||
child: Child::ContactList(cx.add_view(|cx| {
|
||||
ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx)
|
||||
})),
|
||||
project,
|
||||
user_store,
|
||||
workspace,
|
||||
_subscription: None,
|
||||
};
|
||||
this.show_contact_list(String::new(), cx);
|
||||
@@ -50,19 +57,19 @@ impl ContactsPopover {
|
||||
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
|
||||
match &self.child {
|
||||
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)
|
||||
}
|
||||
Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), 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)
|
||||
let finder = build_contact_finder(self.user_store.clone(), cx);
|
||||
finder.set_query(editor_text, cx);
|
||||
finder
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
PickerEvent::Dismiss => cx.emit(Event::Dismissed),
|
||||
}));
|
||||
self.child = Child::ContactFinder(child);
|
||||
cx.notify();
|
||||
@@ -70,12 +77,20 @@ impl ContactsPopover {
|
||||
|
||||
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)
|
||||
ContactList::new(
|
||||
self.project.clone(),
|
||||
self.user_store.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
.with_editor_text(editor_text, cx)
|
||||
});
|
||||
cx.focus(&child);
|
||||
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
|
||||
self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event {
|
||||
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
crate::contact_list::Event::ToggleContactFinder => {
|
||||
this.toggle_contact_finder(&Default::default(), cx)
|
||||
}
|
||||
}));
|
||||
self.child = Child::ContactList(child);
|
||||
cx.notify();
|
||||
@@ -91,27 +106,24 @@ impl View for ContactsPopover {
|
||||
"ContactsPopover"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = theme::current(cx).clone();
|
||||
let child = match &self.child {
|
||||
Child::ContactList(child) => ChildView::new(child, cx),
|
||||
Child::ContactFinder(child) => ChildView::new(child, cx),
|
||||
};
|
||||
|
||||
MouseEventHandler::<ContactsPopover>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::<ContactsPopover, Self>::new(0, cx, |_, _| {
|
||||
Flex::column()
|
||||
.with_child(child.flex(1., true).boxed())
|
||||
.with_child(child.flex(1., true))
|
||||
.contained()
|
||||
.with_style(theme.contacts_popover.container)
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
.boxed()
|
||||
})
|
||||
.on_down_out(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(ToggleContactsMenu);
|
||||
})
|
||||
.boxed()
|
||||
.on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -7,12 +7,14 @@ use gpui::{
|
||||
},
|
||||
json::ToJson,
|
||||
serde_json::{self, json},
|
||||
Axis, DebugContext, Element, ElementBox, MeasurementContext, PaintContext,
|
||||
AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext,
|
||||
};
|
||||
|
||||
use crate::CollabTitlebarItem;
|
||||
|
||||
pub(crate) struct FacePile {
|
||||
overlap: f32,
|
||||
faces: Vec<ElementBox>,
|
||||
faces: Vec<AnyElement<CollabTitlebarItem>>,
|
||||
}
|
||||
|
||||
impl FacePile {
|
||||
@@ -24,20 +26,21 @@ impl FacePile {
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for FacePile {
|
||||
impl Element<CollabTitlebarItem> for FacePile {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
cx: &mut gpui::LayoutContext,
|
||||
view: &mut CollabTitlebarItem,
|
||||
cx: &mut LayoutContext<CollabTitlebarItem>,
|
||||
) -> (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 += face.layout(constraint, view, cx).x();
|
||||
}
|
||||
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
@@ -46,10 +49,12 @@ impl Element for FacePile {
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
scene: &mut SceneBuilder,
|
||||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
cx: &mut PaintContext,
|
||||
view: &mut CollabTitlebarItem,
|
||||
cx: &mut ViewContext<CollabTitlebarItem>,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
@@ -59,8 +64,8 @@ impl Element for FacePile {
|
||||
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);
|
||||
scene.paint_layer(None, |scene| {
|
||||
face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
|
||||
});
|
||||
origin_x += self.overlap;
|
||||
}
|
||||
@@ -75,7 +80,8 @@ impl Element for FacePile {
|
||||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &MeasurementContext,
|
||||
_: &CollabTitlebarItem,
|
||||
_: &ViewContext<CollabTitlebarItem>,
|
||||
) -> Option<RectF> {
|
||||
None
|
||||
}
|
||||
@@ -85,7 +91,8 @@ impl Element for FacePile {
|
||||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &DebugContext,
|
||||
_: &CollabTitlebarItem,
|
||||
_: &ViewContext<CollabTitlebarItem>,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "FacePile",
|
||||
@@ -94,8 +101,8 @@ impl Element for FacePile {
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<ElementBox> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = ElementBox>>(&mut self, children: T) {
|
||||
impl Extend<AnyElement<CollabTitlebarItem>> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = AnyElement<CollabTitlebarItem>>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use client::proto;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
impl_internal_actions,
|
||||
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
|
||||
AppContext, Entity, RenderContext, View, ViewContext,
|
||||
AnyElement, AppContext, Entity, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use util::ResultExt;
|
||||
use workspace::JoinProject;
|
||||
|
||||
impl_internal_actions!(incoming_call_notification, [RespondToCall]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(IncomingCallNotification::respond_to_call);
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
let app_state = Arc::downgrade(app_state);
|
||||
let mut incoming_call = ActiveCall::global(cx).read(cx).incoming();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let mut notification_windows = Vec::new();
|
||||
@@ -28,7 +25,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
if let Some(incoming_call) = incoming_call {
|
||||
const PADDING: f32 = 16.;
|
||||
let window_size = cx.read(|cx| {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
let theme = &theme::current(cx).incoming_call_notification;
|
||||
vec2f(theme.window_width, theme.window_height)
|
||||
});
|
||||
|
||||
@@ -44,11 +41,12 @@ pub fn init(cx: &mut AppContext) {
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
},
|
||||
|_| IncomingCallNotification::new(incoming_call.clone()),
|
||||
|_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
|
||||
);
|
||||
|
||||
notification_windows.push(window_id);
|
||||
@@ -66,32 +64,40 @@ struct RespondToCall {
|
||||
|
||||
pub struct IncomingCallNotification {
|
||||
call: IncomingCall,
|
||||
app_state: Weak<AppState>,
|
||||
}
|
||||
|
||||
impl IncomingCallNotification {
|
||||
pub fn new(call: IncomingCall) -> Self {
|
||||
Self { call }
|
||||
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
|
||||
Self { call, app_state }
|
||||
}
|
||||
|
||||
fn respond_to_call(&mut self, action: &RespondToCall, cx: &mut ViewContext<Self>) {
|
||||
fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
if action.accept {
|
||||
if accept {
|
||||
let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx));
|
||||
let caller_user_id = self.call.calling_user.id;
|
||||
let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id);
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
join.await?;
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
cx.dispatch_global_action(JoinProject {
|
||||
project_id,
|
||||
follow_user_id: caller_user_id,
|
||||
})
|
||||
});
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
let app_state = self.app_state.clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
join.await?;
|
||||
if let Some(project_id) = initial_project_id {
|
||||
cx.update(|cx| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
workspace::join_remote_project(
|
||||
project_id,
|
||||
caller_user_id,
|
||||
app_state,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
} else {
|
||||
active_call.update(cx, |active_call, _| {
|
||||
active_call.decline_incoming().log_err();
|
||||
@@ -99,8 +105,8 @@ impl IncomingCallNotification {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_caller(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
fn render_caller(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).incoming_call_notification;
|
||||
let default_project = proto::ParticipantProject::default();
|
||||
let initial_project = self
|
||||
.call
|
||||
@@ -112,7 +118,6 @@ impl IncomingCallNotification {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.caller_avatar)
|
||||
.aligned()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
@@ -122,8 +127,7 @@ impl IncomingCallNotification {
|
||||
theme.caller_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_username.container)
|
||||
.boxed(),
|
||||
.with_style(theme.caller_username.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
@@ -138,8 +142,7 @@ impl IncomingCallNotification {
|
||||
theme.caller_message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_message.container)
|
||||
.boxed(),
|
||||
.with_style(theme.caller_message.container),
|
||||
)
|
||||
.with_children(if initial_project.worktree_root_names.is_empty() {
|
||||
None
|
||||
@@ -150,66 +153,56 @@ impl IncomingCallNotification {
|
||||
theme.worktree_roots.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.worktree_roots.container)
|
||||
.boxed(),
|
||||
.with_style(theme.worktree_roots.container),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.caller_metadata)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.caller_container)
|
||||
.flex(1., true)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Accept {}
|
||||
enum Decline {}
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::<Accept>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
MouseEventHandler::<Accept, Self>::new(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Accept", theme.accept_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.accept_button.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(RespondToCall { accept: true });
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.respond(true, cx);
|
||||
})
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Decline>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.incoming_call_notification;
|
||||
MouseEventHandler::<Decline, Self>::new(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Decline", theme.decline_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.decline_button.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(RespondToCall { accept: false });
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.respond(false, cx);
|
||||
})
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(
|
||||
cx.global::<Settings>()
|
||||
.theme
|
||||
.incoming_call_notification
|
||||
.button_width,
|
||||
)
|
||||
.boxed()
|
||||
.with_width(theme.incoming_call_notification.button_width)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,19 +215,14 @@ impl View for IncomingCallNotification {
|
||||
"IncomingCallNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||
let background = cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
.incoming_call_notification
|
||||
.background;
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let background = theme::current(cx).incoming_call_notification.background;
|
||||
Flex::row()
|
||||
.with_child(self.render_caller(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.expanded()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,23 +2,26 @@ use client::User;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, Element, ElementBox, RenderContext, View,
|
||||
AnyElement, Element, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
|
||||
enum Dismiss {}
|
||||
enum Button {}
|
||||
|
||||
pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
pub fn render_user_notification<F, V>(
|
||||
user: Arc<User>,
|
||||
title: &'static str,
|
||||
body: Option<&'static str>,
|
||||
dismiss_action: A,
|
||||
buttons: Vec<(&'static str, Box<dyn Action>)>,
|
||||
cx: &mut RenderContext<V>,
|
||||
) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
on_dismiss: F,
|
||||
buttons: Vec<(&'static str, Box<dyn Fn(&mut V, &mut ViewContext<V>)>)>,
|
||||
cx: &mut ViewContext<V>,
|
||||
) -> AnyElement<V>
|
||||
where
|
||||
F: 'static + Fn(&mut V, &mut ViewContext<V>),
|
||||
V: View,
|
||||
{
|
||||
let theme = theme::current(cx).clone();
|
||||
let theme = &theme.contact_notification;
|
||||
|
||||
Flex::column()
|
||||
@@ -35,7 +38,6 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
)
|
||||
.aligned()
|
||||
.top()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Text::new(
|
||||
@@ -47,11 +49,10 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
.aligned()
|
||||
.top()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Dismiss>::new(user.id as usize, cx, |state, _| {
|
||||
MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state, false);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
@@ -63,13 +64,10 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(dismiss_action.boxed_clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, view, cx| on_dismiss(view, cx))
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(
|
||||
@@ -78,16 +76,14 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
)
|
||||
.aligned()
|
||||
.top()
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
.flex_float(),
|
||||
)
|
||||
.named("contact notification header"),
|
||||
.into_any_named("contact notification header"),
|
||||
)
|
||||
.with_children(body.map(|body| {
|
||||
Label::new(body, theme.body_message.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.body_message.container)
|
||||
.boxed()
|
||||
}))
|
||||
.with_children(if buttons.is_empty() {
|
||||
None
|
||||
@@ -95,26 +91,21 @@ pub fn render_user_notification<V: View, A: Action + Clone>(
|
||||
Some(
|
||||
Flex::row()
|
||||
.with_children(buttons.into_iter().enumerate().map(
|
||||
|(ix, (message, action))| {
|
||||
MouseEventHandler::<Button>::new(ix, cx, |state, _| {
|
||||
|(ix, (message, handler))| {
|
||||
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
|
||||
let button = theme.button.style_for(state, false);
|
||||
Label::new(message, button.text.clone())
|
||||
.contained()
|
||||
.with_style(button.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_any_action(action.boxed_clone())
|
||||
})
|
||||
.boxed()
|
||||
.on_click(MouseButton::Left, move |_, view, cx| handler(view, cx))
|
||||
},
|
||||
))
|
||||
.aligned()
|
||||
.right()
|
||||
.boxed(),
|
||||
.right(),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
@@ -2,22 +2,16 @@ use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
|
||||
AppContext, Entity, RenderContext, View, ViewContext,
|
||||
AppContext, Entity, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use workspace::JoinProject;
|
||||
|
||||
actions!(project_shared_notification, [DismissProject]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ProjectSharedNotification::join);
|
||||
cx.add_action(ProjectSharedNotification::dismiss);
|
||||
use std::sync::{Arc, Weak};
|
||||
use workspace::AppState;
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
let app_state = Arc::downgrade(app_state);
|
||||
let active_call = ActiveCall::global(cx);
|
||||
let mut notification_windows = HashMap::default();
|
||||
cx.subscribe(&active_call, move |_, event, cx| match event {
|
||||
@@ -27,7 +21,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
worktree_root_names,
|
||||
} => {
|
||||
const PADDING: f32 = 16.;
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
@@ -41,6 +35,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
@@ -50,6 +45,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
@@ -62,14 +58,14 @@ pub fn init(cx: &mut AppContext) {
|
||||
room::Event::RemoteProjectUnshared { project_id } => {
|
||||
if let Some(window_ids) = notification_windows.remove(&project_id) {
|
||||
for window_id in window_ids {
|
||||
cx.remove_window(window_id);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
}
|
||||
}
|
||||
room::Event::Left => {
|
||||
for (_, window_ids) in notification_windows.drain() {
|
||||
for window_id in window_ids {
|
||||
cx.remove_window(window_id);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,36 +78,43 @@ pub struct ProjectSharedNotification {
|
||||
project_id: u64,
|
||||
worktree_root_names: Vec<String>,
|
||||
owner: Arc<User>,
|
||||
app_state: Weak<AppState>,
|
||||
}
|
||||
|
||||
impl ProjectSharedNotification {
|
||||
fn new(owner: Arc<User>, project_id: u64, worktree_root_names: Vec<String>) -> Self {
|
||||
fn new(
|
||||
owner: Arc<User>,
|
||||
project_id: u64,
|
||||
worktree_root_names: Vec<String>,
|
||||
app_state: Weak<AppState>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project_id,
|
||||
worktree_root_names,
|
||||
owner,
|
||||
app_state,
|
||||
}
|
||||
}
|
||||
|
||||
fn join(&mut self, _: &JoinProject, cx: &mut ViewContext<Self>) {
|
||||
let window_id = cx.window_id();
|
||||
cx.remove_window(window_id);
|
||||
cx.propagate_action();
|
||||
fn join(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.remove_window();
|
||||
if let Some(app_state) = self.app_state.upgrade() {
|
||||
workspace::join_remote_project(self.project_id, self.owner.id, app_state, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &DismissProject, cx: &mut ViewContext<Self>) {
|
||||
let window_id = cx.window_id();
|
||||
cx.remove_window(window_id);
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.remove_window();
|
||||
}
|
||||
|
||||
fn render_owner(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
fn render_owner(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
Flex::row()
|
||||
.with_children(self.owner.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.owner_avatar)
|
||||
.aligned()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
@@ -121,8 +124,7 @@ impl ProjectSharedNotification {
|
||||
theme.owner_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_username.container)
|
||||
.boxed(),
|
||||
.with_style(theme.owner_username.container),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
@@ -137,8 +139,7 @@ impl ProjectSharedNotification {
|
||||
theme.message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.message.container)
|
||||
.boxed(),
|
||||
.with_style(theme.message.container),
|
||||
)
|
||||
.with_children(if self.worktree_root_names.is_empty() {
|
||||
None
|
||||
@@ -149,72 +150,54 @@ impl ProjectSharedNotification {
|
||||
theme.worktree_roots.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.worktree_roots.container)
|
||||
.boxed(),
|
||||
.with_style(theme.worktree_roots.container),
|
||||
)
|
||||
})
|
||||
.contained()
|
||||
.with_style(theme.owner_metadata)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.owner_container)
|
||||
.flex(1., true)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_buttons(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Open {}
|
||||
enum Dismiss {}
|
||||
|
||||
let project_id = self.project_id;
|
||||
let owner_user_id = self.owner.id;
|
||||
|
||||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::<Open>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
MouseEventHandler::<Open, Self>::new(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Open", theme.open_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.open_button.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(JoinProject {
|
||||
project_id,
|
||||
follow_user_id: owner_user_id,
|
||||
});
|
||||
})
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.on_click(MouseButton::Left, move |_, this, cx| this.join(cx))
|
||||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Dismiss>::new(0, cx, |_, cx| {
|
||||
let theme = &cx.global::<Settings>().theme.project_shared_notification;
|
||||
MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.dismiss_button.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(DismissProject);
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.dismiss(cx);
|
||||
})
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
.flex(1., true),
|
||||
)
|
||||
.constrained()
|
||||
.with_width(
|
||||
cx.global::<Settings>()
|
||||
.theme
|
||||
.project_shared_notification
|
||||
.button_width,
|
||||
)
|
||||
.boxed()
|
||||
.with_width(theme.project_shared_notification.button_width)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,18 +210,14 @@ impl View for ProjectSharedNotification {
|
||||
"ProjectSharedNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||
let background = cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
.project_shared_notification
|
||||
.background;
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> gpui::AnyElement<Self> {
|
||||
let background = theme::current(cx).project_shared_notification.background;
|
||||
Flex::row()
|
||||
.with_child(self.render_owner(cx))
|
||||
.with_child(self.render_buttons(cx))
|
||||
.contained()
|
||||
.with_background_color(background)
|
||||
.expanded()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use crate::toggle_screen_sharing;
|
||||
use call::ActiveCall;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{MouseEventHandler, Svg},
|
||||
platform::{Appearance, MouseButton},
|
||||
AppContext, Element, ElementBox, Entity, RenderContext, View,
|
||||
AnyElement, AppContext, Element, Entity, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
use crate::ToggleScreenSharing;
|
||||
use workspace::WorkspaceSettings;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let active_call = ActiveCall::global(cx);
|
||||
@@ -16,14 +15,16 @@ pub fn init(cx: &mut AppContext) {
|
||||
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 {
|
||||
if status_indicator.is_none()
|
||||
&& settings::get::<WorkspaceSettings>(cx).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);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
} else if let Some((window_id, _)) = status_indicator.take() {
|
||||
cx.remove_status_bar_item(window_id);
|
||||
cx.update_window(window_id, |cx| cx.remove_window());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -40,23 +41,22 @@ impl View for SharingStatusIndicator {
|
||||
"SharingStatusIndicator"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
let color = match cx.appearance {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let color = match cx.window_appearance() {
|
||||
Appearance::Light | Appearance::VibrantLight => Color::black(),
|
||||
Appearance::Dark | Appearance::VibrantDark => Color::white(),
|
||||
};
|
||||
|
||||
MouseEventHandler::<Self>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::<Self, 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);
|
||||
.on_click(MouseButton::Left, |_, _, cx| {
|
||||
toggle_screen_sharing(&Default::default(), cx)
|
||||
})
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,9 @@ workspace = { path = "../workspace" }
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_json.workspace = true
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.9"
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
use collections::CommandPaletteFilter;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{ChildView, Flex, Label, ParentElement},
|
||||
keymap_matcher::Keystroke,
|
||||
Action, AnyViewHandle, AppContext, Element, Entity, MouseState, RenderContext, View,
|
||||
ViewContext, ViewHandle,
|
||||
actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState,
|
||||
ViewContext,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::cmp;
|
||||
use util::ResultExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(CommandPalette::toggle);
|
||||
Picker::<CommandPalette>::init(cx);
|
||||
cx.add_action(toggle_command_palette);
|
||||
CommandPalette::init(cx);
|
||||
}
|
||||
|
||||
actions!(command_palette, [Toggle]);
|
||||
|
||||
pub struct CommandPalette {
|
||||
picker: ViewHandle<Picker<Self>>,
|
||||
pub type CommandPalette = Picker<CommandPaletteDelegate>;
|
||||
|
||||
pub struct CommandPaletteDelegate {
|
||||
actions: Vec<Command>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_ix: usize,
|
||||
@@ -42,104 +40,29 @@ struct Command {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
impl CommandPalette {
|
||||
pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
|
||||
let this = cx.weak_handle();
|
||||
let actions = cx
|
||||
.available_actions(cx.window_id(), focused_view_id)
|
||||
.filter_map(|(name, action, bindings)| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
if filter.filtered_namespaces.contains(action.namespace()) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
fn toggle_command_palette(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| cx.view_id());
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id), cx))
|
||||
});
|
||||
}
|
||||
|
||||
Some(Command {
|
||||
name: humanize_action_name(name),
|
||||
action,
|
||||
keystrokes: bindings
|
||||
.iter()
|
||||
.map(|binding| binding.keystrokes())
|
||||
.last()
|
||||
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let picker = cx.add_view(|cx| Picker::new("Execute a command...", this, cx));
|
||||
impl CommandPaletteDelegate {
|
||||
pub fn new(focused_view_id: usize) -> Self {
|
||||
Self {
|
||||
picker,
|
||||
actions,
|
||||
actions: Default::default(),
|
||||
matches: vec![],
|
||||
selected_ix: 0,
|
||||
focused_view_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
let workspace = cx.handle();
|
||||
let window_id = cx.window_id();
|
||||
let focused_view_id = cx
|
||||
.focused_view_id(window_id)
|
||||
.unwrap_or_else(|| workspace.id());
|
||||
|
||||
cx.as_mut().defer(move |cx| {
|
||||
let this = cx.add_view(&workspace, |cx| Self::new(focused_view_id, cx));
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
cx.subscribe(&this, Self::on_event).detach();
|
||||
this
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<Self>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Dismissed => workspace.dismiss_modal(cx),
|
||||
Event::Confirmed {
|
||||
window_id,
|
||||
focused_view_id,
|
||||
action,
|
||||
} => {
|
||||
let window_id = *window_id;
|
||||
let focused_view_id = *focused_view_id;
|
||||
let action = action.boxed_clone();
|
||||
workspace.dismiss_modal(cx);
|
||||
cx.as_mut()
|
||||
.defer(move |cx| cx.dispatch_any_action_at(window_id, focused_view_id, action))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for CommandPalette {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for CommandPalette {
|
||||
fn ui_name() -> &'static str {
|
||||
"CommandPalette"
|
||||
impl PickerDelegate for CommandPaletteDelegate {
|
||||
fn placeholder_text(&self) -> std::sync::Arc<str> {
|
||||
"Execute a command...".into()
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> gpui::ElementBox {
|
||||
ChildView::new(&self.picker, cx).boxed()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for CommandPalette {
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
@@ -148,26 +71,55 @@ impl PickerDelegate for CommandPalette {
|
||||
self.selected_ix
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) {
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_ix = ix;
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
cx: &mut gpui::ViewContext<Self>,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let candidates = self
|
||||
.actions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: command.name.to_string(),
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let window_id = cx.window_id();
|
||||
let view_id = self.focused_view_id;
|
||||
cx.spawn(move |picker, mut cx| async move {
|
||||
let actions = cx
|
||||
.available_actions(window_id, view_id)
|
||||
.into_iter()
|
||||
.filter_map(|(name, action, bindings)| {
|
||||
let filtered = cx.read(|cx| {
|
||||
if cx.has_global::<CommandPaletteFilter>() {
|
||||
let filter = cx.global::<CommandPaletteFilter>();
|
||||
filter.filtered_namespaces.contains(action.namespace())
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if filtered {
|
||||
None
|
||||
} else {
|
||||
Some(Command {
|
||||
name: humanize_action_name(name),
|
||||
action,
|
||||
keystrokes: bindings
|
||||
.iter()
|
||||
.map(|binding| binding.keystrokes())
|
||||
.last()
|
||||
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let candidates = actions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate {
|
||||
id: ix,
|
||||
string: command.name.to_string(),
|
||||
char_bag: command.name.chars().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
@@ -190,32 +142,37 @@ impl PickerDelegate for CommandPalette {
|
||||
)
|
||||
.await
|
||||
};
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.matches = matches;
|
||||
if this.matches.is_empty() {
|
||||
this.selected_ix = 0;
|
||||
} else {
|
||||
this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1);
|
||||
}
|
||||
});
|
||||
picker
|
||||
.update(&mut cx, |picker, _| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.actions = actions;
|
||||
delegate.matches = matches;
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_ix = 0;
|
||||
} else {
|
||||
delegate.selected_ix =
|
||||
cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if !self.matches.is_empty() {
|
||||
let window_id = cx.window_id();
|
||||
let focused_view_id = self.focused_view_id;
|
||||
let action_ix = self.matches[self.selected_ix].candidate_id;
|
||||
cx.emit(Event::Confirmed {
|
||||
window_id: cx.window_id(),
|
||||
focused_view_id: self.focused_view_id,
|
||||
action: self.actions.remove(action_ix).action,
|
||||
});
|
||||
} else {
|
||||
cx.emit(Event::Dismissed);
|
||||
let action = self.actions.remove(action_ix).action;
|
||||
cx.app_context()
|
||||
.spawn(move |mut cx| async move {
|
||||
cx.dispatch_action(window_id, focused_view_id, action.as_ref())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
@@ -224,11 +181,10 @@ impl PickerDelegate for CommandPalette {
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> gpui::ElementBox {
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let mat = &self.matches[ix];
|
||||
let command = &self.actions[mat.candidate_id];
|
||||
let settings = cx.global::<Settings>();
|
||||
let theme = &settings.theme;
|
||||
let theme = theme::current(cx);
|
||||
let style = theme.picker.item.style_for(mouse_state, selected);
|
||||
let key_style = &theme.command_palette.key.style_for(mouse_state, selected);
|
||||
let keystroke_spacing = theme.command_palette.keystroke_spacing;
|
||||
@@ -236,8 +192,7 @@ impl PickerDelegate for CommandPalette {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(mat.string.clone(), style.label.clone())
|
||||
.with_highlights(mat.positions.clone())
|
||||
.boxed(),
|
||||
.with_highlights(mat.positions.clone()),
|
||||
)
|
||||
.with_children(command.keystrokes.iter().map(|keystroke| {
|
||||
Flex::row()
|
||||
@@ -254,8 +209,7 @@ impl PickerDelegate for CommandPalette {
|
||||
Some(
|
||||
Label::new(label, key_style.label.clone())
|
||||
.contained()
|
||||
.with_style(key_style.container)
|
||||
.boxed(),
|
||||
.with_style(key_style.container),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -265,17 +219,15 @@ impl PickerDelegate for CommandPalette {
|
||||
.with_child(
|
||||
Label::new(keystroke.key.clone(), key_style.label.clone())
|
||||
.contained()
|
||||
.with_style(key_style.container)
|
||||
.boxed(),
|
||||
.with_style(key_style.container),
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(keystroke_spacing)
|
||||
.flex_float()
|
||||
.boxed()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,9 +266,11 @@ impl std::fmt::Debug for Command {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use editor::Editor;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{executor::Deterministic, TestAppContext};
|
||||
use project::Project;
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
@@ -337,18 +291,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_command_palette(cx: &mut TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
|
||||
cx.update(|cx| {
|
||||
editor::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(cx);
|
||||
});
|
||||
async fn test_command_palette(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let editor = cx.add_view(&workspace, |cx| {
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let editor = cx.add_view(window_id, |cx| {
|
||||
let mut editor = Editor::single_line(None, cx);
|
||||
editor.set_text("abc", cx);
|
||||
editor
|
||||
@@ -360,7 +308,7 @@ mod tests {
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
CommandPalette::toggle(workspace, &Toggle, cx)
|
||||
toggle_command_palette(workspace, &Toggle, cx);
|
||||
});
|
||||
|
||||
let palette = workspace.read_with(cx, |workspace, _| {
|
||||
@@ -369,15 +317,17 @@ mod tests {
|
||||
|
||||
palette
|
||||
.update(cx, |palette, cx| {
|
||||
palette.update_matches("bcksp".to_string(), cx)
|
||||
palette
|
||||
.delegate_mut()
|
||||
.update_matches("bcksp".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
palette.update(cx, |palette, cx| {
|
||||
assert_eq!(palette.matches[0].string, "editor: backspace");
|
||||
palette.confirm(cx);
|
||||
assert_eq!(palette.delegate().matches[0].string, "editor: backspace");
|
||||
palette.confirm(&Default::default(), cx);
|
||||
});
|
||||
|
||||
deterministic.run_until_parked();
|
||||
editor.read_with(cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "ab");
|
||||
});
|
||||
@@ -390,7 +340,7 @@ mod tests {
|
||||
});
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
CommandPalette::toggle(workspace, &Toggle, cx);
|
||||
toggle_command_palette(workspace, &Toggle, cx);
|
||||
});
|
||||
|
||||
// Assert editor command not present
|
||||
@@ -400,10 +350,26 @@ mod tests {
|
||||
|
||||
palette
|
||||
.update(cx, |palette, cx| {
|
||||
palette.update_matches("bcksp".to_string(), cx)
|
||||
palette
|
||||
.delegate_mut()
|
||||
.update_matches("bcksp".to_string(), cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
palette.update(cx, |palette, _| assert!(palette.matches.is_empty()));
|
||||
palette.update(cx, |palette, _| {
|
||||
assert!(palette.delegate().matches.is_empty())
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
|
||||
cx.update(|cx| {
|
||||
let app_state = AppState::test(cx);
|
||||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(cx);
|
||||
app_state
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,4 @@ gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
smallvec = "1.6"
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -1,55 +1,71 @@
|
||||
use gpui::{
|
||||
anyhow,
|
||||
elements::*,
|
||||
geometry::vector::Vector2F,
|
||||
impl_internal_actions,
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
Action, AnyViewHandle, AppContext, Axis, Entity, MouseState, RenderContext, SizeConstraint,
|
||||
Subscription, View, ViewContext,
|
||||
Action, AnyViewHandle, AppContext, Axis, Entity, MouseState, SizeConstraint, Subscription,
|
||||
View, ViewContext,
|
||||
};
|
||||
use menu::*;
|
||||
use settings::Settings;
|
||||
use std::{any::TypeId, borrow::Cow, time::Duration};
|
||||
|
||||
pub type StaticItem = Box<dyn Fn(&mut AppContext) -> ElementBox>;
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
struct Clicked;
|
||||
|
||||
impl_internal_actions!(context_menu, [Clicked]);
|
||||
use std::{any::TypeId, borrow::Cow, sync::Arc, time::Duration};
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ContextMenu::select_first);
|
||||
cx.add_action(ContextMenu::select_last);
|
||||
cx.add_action(ContextMenu::select_next);
|
||||
cx.add_action(ContextMenu::select_prev);
|
||||
cx.add_action(ContextMenu::clicked);
|
||||
cx.add_action(ContextMenu::confirm);
|
||||
cx.add_action(ContextMenu::cancel);
|
||||
}
|
||||
|
||||
type ContextMenuItemBuilder = Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> ElementBox>;
|
||||
pub type StaticItem = Box<dyn Fn(&mut AppContext) -> AnyElement<ContextMenu>>;
|
||||
|
||||
type ContextMenuItemBuilder =
|
||||
Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>>;
|
||||
|
||||
pub enum ContextMenuItemLabel {
|
||||
String(Cow<'static, str>),
|
||||
Element(ContextMenuItemBuilder),
|
||||
}
|
||||
|
||||
pub enum ContextMenuAction {
|
||||
ParentAction {
|
||||
action: Box<dyn Action>,
|
||||
},
|
||||
ViewAction {
|
||||
action: Box<dyn Action>,
|
||||
for_view: usize,
|
||||
},
|
||||
impl From<Cow<'static, str>> for ContextMenuItemLabel {
|
||||
fn from(s: Cow<'static, str>) -> Self {
|
||||
Self::String(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextMenuAction {
|
||||
fn id(&self) -> TypeId {
|
||||
impl From<&'static str> for ContextMenuItemLabel {
|
||||
fn from(s: &'static str) -> Self {
|
||||
Self::String(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ContextMenuItemLabel {
|
||||
fn from(s: String) -> Self {
|
||||
Self::String(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for ContextMenuItemLabel
|
||||
where
|
||||
T: 'static + Fn(&mut MouseState, &theme::ContextMenuItem) -> AnyElement<ContextMenu>,
|
||||
{
|
||||
fn from(f: T) -> Self {
|
||||
Self::Element(Box::new(f))
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextMenuItemAction {
|
||||
Action(Box<dyn Action>),
|
||||
Handler(Arc<dyn Fn(&mut ViewContext<ContextMenu>)>),
|
||||
}
|
||||
|
||||
impl Clone for ContextMenuItemAction {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
ContextMenuAction::ParentAction { action } => action.id(),
|
||||
ContextMenuAction::ViewAction { action, .. } => action.id(),
|
||||
Self::Action(action) => Self::Action(action.boxed_clone()),
|
||||
Self::Handler(handler) => Self::Handler(handler.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,42 +73,27 @@ impl ContextMenuAction {
|
||||
pub enum ContextMenuItem {
|
||||
Item {
|
||||
label: ContextMenuItemLabel,
|
||||
action: ContextMenuAction,
|
||||
action: ContextMenuItemAction,
|
||||
},
|
||||
Static(StaticItem),
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self {
|
||||
pub fn action(label: impl Into<ContextMenuItemLabel>, action: impl 'static + Action) -> Self {
|
||||
Self::Item {
|
||||
label: ContextMenuItemLabel::Element(label),
|
||||
action: ContextMenuAction::ParentAction {
|
||||
action: Box::new(action),
|
||||
},
|
||||
label: label.into(),
|
||||
action: ContextMenuItemAction::Action(Box::new(action)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
|
||||
Self::Item {
|
||||
label: ContextMenuItemLabel::String(label.into()),
|
||||
action: ContextMenuAction::ParentAction {
|
||||
action: Box::new(action),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_for_view(
|
||||
label: impl Into<Cow<'static, str>>,
|
||||
view_id: usize,
|
||||
action: impl 'static + Action,
|
||||
pub fn handler(
|
||||
label: impl Into<ContextMenuItemLabel>,
|
||||
handler: impl 'static + Fn(&mut ViewContext<ContextMenu>),
|
||||
) -> Self {
|
||||
Self::Item {
|
||||
label: ContextMenuItemLabel::String(label.into()),
|
||||
action: ContextMenuAction::ViewAction {
|
||||
action: Box::new(action),
|
||||
for_view: view_id,
|
||||
},
|
||||
label: label.into(),
|
||||
action: ContextMenuItemAction::Handler(Arc::new(handler)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +107,10 @@ impl ContextMenuItem {
|
||||
|
||||
fn action_id(&self) -> Option<TypeId> {
|
||||
match self {
|
||||
ContextMenuItem::Item { action, .. } => Some(action.id()),
|
||||
ContextMenuItem::Item { action, .. } => match action {
|
||||
ContextMenuItemAction::Action(action) => Some(action.id()),
|
||||
ContextMenuItemAction::Handler(_) => None,
|
||||
},
|
||||
ContextMenuItem::Static(..) | ContextMenuItem::Separator => None,
|
||||
}
|
||||
}
|
||||
@@ -121,7 +125,6 @@ pub struct ContextMenu {
|
||||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
previously_focused_view_id: Option<usize>,
|
||||
clicked: bool,
|
||||
parent_view_id: usize,
|
||||
_actions_observation: Subscription,
|
||||
}
|
||||
@@ -135,29 +138,27 @@ impl View for ContextMenu {
|
||||
"ContextMenu"
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.add_identifier("menu");
|
||||
cx
|
||||
fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) {
|
||||
Self::reset_to_default_keymap_context(keymap);
|
||||
keymap.add_identifier("menu");
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if !self.visible {
|
||||
return Empty::new().boxed();
|
||||
return Empty::new().into_any();
|
||||
}
|
||||
|
||||
// Render the menu once at minimum width.
|
||||
let mut collapsed_menu = self.render_menu_for_measurement(cx).boxed();
|
||||
let expanded_menu = self
|
||||
.render_menu(cx)
|
||||
.constrained()
|
||||
.dynamically(move |constraint, cx| {
|
||||
SizeConstraint::strict_along(
|
||||
Axis::Horizontal,
|
||||
collapsed_menu.layout(constraint, cx).x(),
|
||||
)
|
||||
})
|
||||
.boxed();
|
||||
let mut collapsed_menu = self.render_menu_for_measurement(cx);
|
||||
let expanded_menu =
|
||||
self.render_menu(cx)
|
||||
.constrained()
|
||||
.dynamically(move |constraint, view, cx| {
|
||||
SizeConstraint::strict_along(
|
||||
Axis::Horizontal,
|
||||
collapsed_menu.layout(constraint, view, cx).0.x(),
|
||||
)
|
||||
});
|
||||
|
||||
Overlay::new(expanded_menu)
|
||||
.with_hoverable(true)
|
||||
@@ -165,7 +166,7 @@ impl View for ContextMenu {
|
||||
.with_anchor_position(self.anchor_position)
|
||||
.with_anchor_corner(self.anchor_corner)
|
||||
.with_position_mode(self.position_mode)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
@@ -174,9 +175,7 @@ impl View for ContextMenu {
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let parent_view_id = cx.parent().unwrap();
|
||||
|
||||
pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
show_count: 0,
|
||||
anchor_position: Default::default(),
|
||||
@@ -186,7 +185,6 @@ impl ContextMenu {
|
||||
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),
|
||||
}
|
||||
@@ -202,36 +200,33 @@ impl ContextMenu {
|
||||
.iter()
|
||||
.position(|item| item.action_id() == Some(action_id))
|
||||
{
|
||||
if self.clicked {
|
||||
self.cancel(&Default::default(), cx);
|
||||
} else {
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background().timer(Duration::from_millis(50)).await;
|
||||
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx));
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
self.selected_index = Some(ix);
|
||||
cx.notify();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
cx.background().timer(Duration::from_millis(50)).await;
|
||||
this.update(&mut cx, |this, cx| this.cancel(&Default::default(), cx))?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn clicked(&mut self, _: &Clicked, _: &mut ViewContext<Self>) {
|
||||
self.clicked = true;
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
|
||||
match action {
|
||||
ContextMenuAction::ParentAction { action } => {
|
||||
cx.dispatch_any_action(action.boxed_clone())
|
||||
}
|
||||
ContextMenuAction::ViewAction { action, for_view } => {
|
||||
ContextMenuItemAction::Action(action) => {
|
||||
let window_id = cx.window_id();
|
||||
cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone())
|
||||
let view_id = self.parent_view_id;
|
||||
let action = action.boxed_clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
cx.dispatch_action(window_id, view_id, action.as_ref())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
};
|
||||
ContextMenuItemAction::Handler(handler) => handler(cx),
|
||||
}
|
||||
self.reset(cx);
|
||||
}
|
||||
}
|
||||
@@ -252,7 +247,6 @@ impl ContextMenu {
|
||||
self.items.clear();
|
||||
self.visible = false;
|
||||
self.selected_index.take();
|
||||
self.clicked = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -314,7 +308,7 @@ impl ContextMenu {
|
||||
self.visible = true;
|
||||
self.show_count += 1;
|
||||
if !cx.is_self_focused() {
|
||||
self.previously_focused_view_id = cx.focused_view_id(cx.window_id());
|
||||
self.previously_focused_view_id = cx.focused_view_id();
|
||||
}
|
||||
cx.focus_self();
|
||||
} else {
|
||||
@@ -327,45 +321,42 @@ impl ContextMenu {
|
||||
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();
|
||||
fn render_menu_for_measurement(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
|
||||
let style = theme::current(cx).context_menu.clone();
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { label, .. } => {
|
||||
let style = style.item.style_for(
|
||||
&mut Default::default(),
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { label, .. } => {
|
||||
let style = style.item.style_for(
|
||||
&mut Default::default(),
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
|
||||
match label {
|
||||
ContextMenuItemLabel::String(label) => {
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
ContextMenuItemLabel::Element(element) => {
|
||||
element(&mut Default::default(), style)
|
||||
}
|
||||
match label {
|
||||
ContextMenuItemLabel::String(label) => {
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.into_any()
|
||||
}
|
||||
ContextMenuItemLabel::Element(element) => {
|
||||
element(&mut Default::default(), style)
|
||||
}
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
.with_style(style.separator)
|
||||
.constrained()
|
||||
.with_height(1.)
|
||||
.boxed(),
|
||||
}
|
||||
}))
|
||||
.boxed(),
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
.with_style(style.separator)
|
||||
.constrained()
|
||||
.with_height(1.)
|
||||
.into_any(),
|
||||
}
|
||||
})),
|
||||
)
|
||||
.with_child(
|
||||
Flex::column()
|
||||
@@ -376,26 +367,20 @@ impl ContextMenu {
|
||||
&mut Default::default(),
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
let (action, view_id) = match action {
|
||||
ContextMenuAction::ParentAction { action } => {
|
||||
(action.boxed_clone(), self.parent_view_id)
|
||||
}
|
||||
ContextMenuAction::ViewAction { action, for_view } => {
|
||||
(action.boxed_clone(), *for_view)
|
||||
}
|
||||
};
|
||||
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
)
|
||||
.boxed()
|
||||
match action {
|
||||
ContextMenuItemAction::Action(action) => KeystrokeLabel::new(
|
||||
self.parent_view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
)
|
||||
.into_any(),
|
||||
ContextMenuItemAction::Handler(_) => Empty::new().into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(_) => Empty::new().boxed(),
|
||||
ContextMenuItem::Static(_) => Empty::new().into_any(),
|
||||
|
||||
ContextMenuItem::Separator => Empty::new()
|
||||
.collapsed()
|
||||
@@ -403,78 +388,84 @@ impl ContextMenu {
|
||||
.with_height(1.)
|
||||
.contained()
|
||||
.with_style(style.separator)
|
||||
.boxed(),
|
||||
.into_any(),
|
||||
}
|
||||
}))
|
||||
.contained()
|
||||
.with_margin_left(style.keystroke_margin)
|
||||
.boxed(),
|
||||
.with_margin_left(style.keystroke_margin),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
}
|
||||
|
||||
fn render_menu(&self, cx: &mut RenderContext<Self>) -> impl Element {
|
||||
fn render_menu(&self, cx: &mut ViewContext<Self>) -> impl Element<ContextMenu> {
|
||||
enum Menu {}
|
||||
enum MenuItem {}
|
||||
|
||||
let style = cx.global::<Settings>().theme.context_menu.clone();
|
||||
let style = theme::current(cx).context_menu.clone();
|
||||
|
||||
let window_id = cx.window_id();
|
||||
MouseEventHandler::<Menu>::new(0, cx, |_, cx| {
|
||||
MouseEventHandler::<Menu, ContextMenu>::new(0, cx, |_, cx| {
|
||||
Flex::column()
|
||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { label, action } => {
|
||||
let (action, view_id) = match action {
|
||||
ContextMenuAction::ParentAction { action } => {
|
||||
(action.boxed_clone(), self.parent_view_id)
|
||||
}
|
||||
ContextMenuAction::ViewAction { action, for_view } => {
|
||||
(action.boxed_clone(), *for_view)
|
||||
}
|
||||
};
|
||||
|
||||
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
|
||||
let action = action.clone();
|
||||
let view_id = self.parent_view_id;
|
||||
MouseEventHandler::<MenuItem, ContextMenu>::new(ix, cx, |state, _| {
|
||||
let style =
|
||||
style.item.style_for(state, Some(ix) == self.selected_index);
|
||||
let keystroke = match &action {
|
||||
ContextMenuItemAction::Action(action) => Some(
|
||||
KeystrokeLabel::new(
|
||||
view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
)
|
||||
.flex_float(),
|
||||
),
|
||||
ContextMenuItemAction::Handler(_) => None,
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_child(match label {
|
||||
ContextMenuItemLabel::String(label) => {
|
||||
Label::new(label.clone(), style.label.clone())
|
||||
.contained()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
ContextMenuItemLabel::Element(element) => {
|
||||
element(state, style)
|
||||
}
|
||||
})
|
||||
.with_child({
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
)
|
||||
.flex_float()
|
||||
.boxed()
|
||||
})
|
||||
.with_children(keystroke)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_up(MouseButton::Left, |_, _| {}) // Capture these events
|
||||
.on_down(MouseButton::Left, |_, _| {}) // Capture these events
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(Clicked);
|
||||
.on_up(MouseButton::Left, |_, _, _| {}) // Capture these events
|
||||
.on_down(MouseButton::Left, |_, _, _| {}) // Capture these events
|
||||
.on_click(MouseButton::Left, move |_, menu, cx| {
|
||||
menu.cancel(&Default::default(), cx);
|
||||
let window_id = cx.window_id();
|
||||
cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
|
||||
match &action {
|
||||
ContextMenuItemAction::Action(action) => {
|
||||
let action = action.boxed_clone();
|
||||
cx.app_context()
|
||||
.spawn(|mut cx| async move {
|
||||
cx.dispatch_action(
|
||||
window_id,
|
||||
view_id,
|
||||
action.as_ref(),
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
ContextMenuItemAction::Handler(handler) => handler(cx),
|
||||
}
|
||||
})
|
||||
.on_drag(MouseButton::Left, |_, _| {})
|
||||
.boxed()
|
||||
.on_drag(MouseButton::Left, |_, _, _| {})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
@@ -484,14 +475,17 @@ impl ContextMenu {
|
||||
.with_height(1.)
|
||||
.contained()
|
||||
.with_style(style.separator)
|
||||
.boxed(),
|
||||
.into_any(),
|
||||
}
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_down_out(MouseButton::Left, |_, cx| cx.dispatch_action(Cancel))
|
||||
.on_down_out(MouseButton::Right, |_, cx| cx.dispatch_action(Cancel))
|
||||
.on_down_out(MouseButton::Left, |_, this, cx| {
|
||||
this.cancel(&Default::default(), cx);
|
||||
})
|
||||
.on_down_out(MouseButton::Right, |_, this, cx| {
|
||||
this.cancel(&Default::default(), cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,18 +30,20 @@ node_runtime = { path = "../node_runtime"}
|
||||
util = { path = "../util" }
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
async-tar = "0.4.2"
|
||||
anyhow = "1.0"
|
||||
log = "0.4"
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
smol = "1.2.5"
|
||||
futures = "0.3"
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -99,14 +99,11 @@ pub struct GetCompletionsParams {
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompletionsDocument {
|
||||
pub source: String,
|
||||
pub tab_size: u32,
|
||||
pub indent_size: u32,
|
||||
pub insert_spaces: bool,
|
||||
pub uri: lsp::Url,
|
||||
pub path: String,
|
||||
pub relative_path: String,
|
||||
pub language_id: String,
|
||||
pub position: lsp::Position,
|
||||
pub version: usize,
|
||||
}
|
||||
@@ -146,8 +143,8 @@ pub enum LogMessage {}
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogMessageParams {
|
||||
pub message: String,
|
||||
pub level: u8,
|
||||
pub message: String,
|
||||
pub metadata_str: String,
|
||||
pub extra: Vec<String>,
|
||||
}
|
||||
@@ -169,3 +166,60 @@ impl lsp::notification::Notification for StatusNotification {
|
||||
type Params = StatusNotificationParams;
|
||||
const METHOD: &'static str = "statusNotification";
|
||||
}
|
||||
|
||||
pub enum SetEditorInfo {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SetEditorInfoParams {
|
||||
pub editor_info: EditorInfo,
|
||||
pub editor_plugin_info: EditorPluginInfo,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for SetEditorInfo {
|
||||
type Params = SetEditorInfoParams;
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "setEditorInfo";
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorPluginInfo {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
pub enum NotifyAccepted {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotifyAcceptedParams {
|
||||
pub uuid: String,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for NotifyAccepted {
|
||||
type Params = NotifyAcceptedParams;
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "notifyAccepted";
|
||||
}
|
||||
|
||||
pub enum NotifyRejected {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NotifyRejectedParams {
|
||||
pub uuids: Vec<String>,
|
||||
}
|
||||
|
||||
impl lsp::request::Request for NotifyRejected {
|
||||
type Params = NotifyRejectedParams;
|
||||
type Result = String;
|
||||
const METHOD: &'static str = "notifyRejected";
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ use gpui::{
|
||||
elements::*,
|
||||
geometry::rect::RectF,
|
||||
platform::{WindowBounds, WindowKind, WindowOptions},
|
||||
AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle,
|
||||
AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use theme::ui::modal;
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
@@ -17,58 +17,63 @@ struct OpenGithub;
|
||||
const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot";
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
let copilot = Copilot::global(cx).unwrap();
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
|
||||
cx.observe(&copilot, move |copilot, cx| {
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
let mut code_verification: Option<ViewHandle<CopilotCodeVerification>> = None;
|
||||
cx.observe(&copilot, move |copilot, cx| {
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
match &status {
|
||||
crate::Status::SigningIn { prompt } => {
|
||||
if let Some(code_verification_handle) = code_verification.as_mut() {
|
||||
if cx.has_window(code_verification_handle.window_id()) {
|
||||
code_verification_handle.update(cx, |code_verification_view, cx| {
|
||||
code_verification_view.set_status(status, cx)
|
||||
match &status {
|
||||
crate::Status::SigningIn { prompt } => {
|
||||
if let Some(code_verification_handle) = code_verification.as_mut() {
|
||||
let window_id = code_verification_handle.window_id();
|
||||
let updated = cx.update_window(window_id, |cx| {
|
||||
code_verification_handle.update(cx, |code_verification, cx| {
|
||||
code_verification.set_status(status.clone(), cx)
|
||||
});
|
||||
cx.activate_window();
|
||||
});
|
||||
cx.activate_window(code_verification_handle.window_id());
|
||||
} else {
|
||||
create_copilot_auth_window(cx, &status, &mut code_verification);
|
||||
if updated.is_none() {
|
||||
code_verification = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
} else if let Some(_prompt) = prompt {
|
||||
code_verification = Some(create_copilot_auth_window(cx, &status));
|
||||
}
|
||||
} else if let Some(_prompt) = prompt {
|
||||
create_copilot_auth_window(cx, &status, &mut code_verification);
|
||||
}
|
||||
}
|
||||
Status::Authorized | Status::Unauthorized => {
|
||||
if let Some(code_verification) = code_verification.as_ref() {
|
||||
code_verification.update(cx, |code_verification, cx| {
|
||||
code_verification.set_status(status, cx)
|
||||
});
|
||||
Status::Authorized | Status::Unauthorized => {
|
||||
if let Some(code_verification) = code_verification.as_ref() {
|
||||
let window_id = code_verification.window_id();
|
||||
cx.update_window(window_id, |cx| {
|
||||
code_verification.update(cx, |code_verification, cx| {
|
||||
code_verification.set_status(status, cx)
|
||||
});
|
||||
|
||||
cx.platform().activate(true);
|
||||
cx.activate_window(code_verification.window_id());
|
||||
cx.platform().activate(true);
|
||||
cx.activate_window();
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(code_verification) = code_verification.take() {
|
||||
cx.update_window(code_verification.window_id(), |cx| cx.remove_window());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if let Some(code_verification) = code_verification.take() {
|
||||
cx.remove_window(code_verification.window_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_copilot_auth_window(
|
||||
cx: &mut AppContext,
|
||||
status: &Status,
|
||||
code_verification: &mut Option<ViewHandle<CopilotCodeVerification>>,
|
||||
) {
|
||||
let window_size = cx.global::<Settings>().theme.copilot.modal.dimensions();
|
||||
) -> ViewHandle<CopilotCodeVerification> {
|
||||
let window_size = theme::current(cx).copilot.modal.dimensions();
|
||||
let window_options = WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)),
|
||||
titlebar: None,
|
||||
center: true,
|
||||
focus: true,
|
||||
show: true,
|
||||
kind: WindowKind::Normal,
|
||||
is_movable: true,
|
||||
screen: None,
|
||||
@@ -76,16 +81,20 @@ fn create_copilot_auth_window(
|
||||
let (_, view) = cx.add_window(window_options, |_cx| {
|
||||
CopilotCodeVerification::new(status.clone())
|
||||
});
|
||||
*code_verification = Some(view);
|
||||
view
|
||||
}
|
||||
|
||||
pub struct CopilotCodeVerification {
|
||||
status: Status,
|
||||
connect_clicked: bool,
|
||||
}
|
||||
|
||||
impl CopilotCodeVerification {
|
||||
pub fn new(status: Status) -> Self {
|
||||
Self { status }
|
||||
Self {
|
||||
status,
|
||||
connect_clicked: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
|
||||
@@ -96,8 +105,8 @@ impl CopilotCodeVerification {
|
||||
fn render_device_code(
|
||||
data: &PromptUserDeviceFlow,
|
||||
style: &theme::Copilot,
|
||||
cx: &mut gpui::RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element<Self> {
|
||||
let copied = cx
|
||||
.read_from_clipboard()
|
||||
.map(|item| item.text() == &data.user_code)
|
||||
@@ -105,16 +114,17 @@ impl CopilotCodeVerification {
|
||||
|
||||
let device_code_style = &style.auth.prompting.device_code;
|
||||
|
||||
MouseEventHandler::<Self>::new(0, cx, |state, _cx| {
|
||||
MouseEventHandler::<Self, _>::new(0, cx, |state, _cx| {
|
||||
Flex::row()
|
||||
.with_children([
|
||||
.with_child(
|
||||
Label::new(data.user_code.clone(), device_code_style.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(device_code_style.left_container)
|
||||
.constrained()
|
||||
.with_width(device_code_style.left)
|
||||
.boxed(),
|
||||
.with_width(device_code_style.left),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
if copied { "Copied!" } else { "Copy" },
|
||||
device_code_style.cta.style_for(state, false).text.clone(),
|
||||
@@ -123,196 +133,188 @@ impl CopilotCodeVerification {
|
||||
.contained()
|
||||
.with_style(*device_code_style.right_container.style_for(state, false))
|
||||
.constrained()
|
||||
.with_width(device_code_style.right)
|
||||
.boxed(),
|
||||
])
|
||||
.with_width(device_code_style.right),
|
||||
)
|
||||
.contained()
|
||||
.with_style(device_code_style.cta.style_for(state, false).container)
|
||||
.boxed()
|
||||
})
|
||||
.on_click(gpui::platform::MouseButton::Left, {
|
||||
let user_code = data.user_code.clone();
|
||||
move |_, cx| {
|
||||
move |_, _, cx| {
|
||||
cx.platform()
|
||||
.write_to_clipboard(ClipboardItem::new(user_code.clone()));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.with_cursor_style(gpui::platform::CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_prompting_modal(
|
||||
connect_clicked: bool,
|
||||
data: &PromptUserDeviceFlow,
|
||||
style: &theme::Copilot,
|
||||
cx: &mut gpui::RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum ConnectButton {}
|
||||
|
||||
Flex::column()
|
||||
.with_children([
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Label::new(
|
||||
"Enable Copilot by connecting",
|
||||
style.auth.prompting.subheading.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
Label::new(
|
||||
"your existing license.",
|
||||
style.auth.prompting.subheading.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
])
|
||||
.align_children_center()
|
||||
.contained()
|
||||
.with_style(style.auth.prompting.subheading.container)
|
||||
.boxed(),
|
||||
Self::render_device_code(data, &style, cx),
|
||||
.with_style(style.auth.prompting.subheading.container),
|
||||
)
|
||||
.with_child(Self::render_device_code(data, &style, cx))
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Label::new(
|
||||
"Paste this code into GitHub after",
|
||||
style.auth.prompting.hint.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
Label::new(
|
||||
"clicking the button below.",
|
||||
style.auth.prompting.hint.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
])
|
||||
.align_children_center()
|
||||
.contained()
|
||||
.with_style(style.auth.prompting.hint.container.clone())
|
||||
.boxed(),
|
||||
theme::ui::cta_button_with_click(
|
||||
"Connect to GitHub",
|
||||
style.auth.content_width,
|
||||
&style.auth.cta_button,
|
||||
cx,
|
||||
{
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
move |_, cx| cx.platform().open_url(&verification_uri)
|
||||
},
|
||||
)
|
||||
.boxed(),
|
||||
])
|
||||
.with_style(style.auth.prompting.hint.container.clone()),
|
||||
)
|
||||
.with_child(theme::ui::cta_button::<ConnectButton, _, _, _>(
|
||||
if connect_clicked {
|
||||
"Waiting for connection..."
|
||||
} else {
|
||||
"Connect to GitHub"
|
||||
},
|
||||
style.auth.content_width,
|
||||
&style.auth.cta_button,
|
||||
cx,
|
||||
{
|
||||
let verification_uri = data.verification_uri.clone();
|
||||
move |_, verification, cx| {
|
||||
cx.platform().open_url(&verification_uri);
|
||||
verification.connect_clicked = true;
|
||||
}
|
||||
},
|
||||
))
|
||||
.align_children_center()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_enabled_modal(
|
||||
style: &theme::Copilot,
|
||||
cx: &mut gpui::RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
enum DoneButton {}
|
||||
|
||||
let enabled_style = &style.auth.authorized;
|
||||
Flex::column()
|
||||
.with_children([
|
||||
.with_child(
|
||||
Label::new("Copilot Enabled!", enabled_style.subheading.text.clone())
|
||||
.contained()
|
||||
.with_style(enabled_style.subheading.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Label::new(
|
||||
"You can update your settings or",
|
||||
enabled_style.hint.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
Label::new(
|
||||
"sign out from the Copilot menu in",
|
||||
enabled_style.hint.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
Label::new("the status bar.", enabled_style.hint.text.clone())
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(),
|
||||
])
|
||||
.align_children_center()
|
||||
.contained()
|
||||
.with_style(enabled_style.hint.container)
|
||||
.boxed(),
|
||||
theme::ui::cta_button_with_click(
|
||||
"Done",
|
||||
style.auth.content_width,
|
||||
&style.auth.cta_button,
|
||||
cx,
|
||||
|_, cx| {
|
||||
let window_id = cx.window_id();
|
||||
cx.remove_window(window_id)
|
||||
},
|
||||
)
|
||||
.boxed(),
|
||||
])
|
||||
.with_style(enabled_style.hint.container),
|
||||
)
|
||||
.with_child(theme::ui::cta_button::<DoneButton, _, _, _>(
|
||||
"Done",
|
||||
style.auth.content_width,
|
||||
&style.auth.cta_button,
|
||||
cx,
|
||||
|_, _, cx| cx.remove_window(),
|
||||
))
|
||||
.align_children_center()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_unauthorized_modal(
|
||||
style: &theme::Copilot,
|
||||
cx: &mut gpui::RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let unauthorized_style = &style.auth.not_authorized;
|
||||
|
||||
Flex::column()
|
||||
.with_children([
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Label::new(
|
||||
"Enable Copilot by connecting",
|
||||
unauthorized_style.subheading.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
Label::new(
|
||||
"your existing license.",
|
||||
unauthorized_style.subheading.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
])
|
||||
.align_children_center()
|
||||
.contained()
|
||||
.with_style(unauthorized_style.subheading.container)
|
||||
.boxed(),
|
||||
.with_style(unauthorized_style.subheading.container),
|
||||
)
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_children([
|
||||
Label::new(
|
||||
"You must have an active copilot",
|
||||
unauthorized_style.warning.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
Label::new(
|
||||
"license to use it in Zed.",
|
||||
unauthorized_style.warning.text.clone(),
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
])
|
||||
.align_children_center()
|
||||
.contained()
|
||||
.with_style(unauthorized_style.warning.container)
|
||||
.boxed(),
|
||||
theme::ui::cta_button_with_click(
|
||||
"Subscribe on GitHub",
|
||||
style.auth.content_width,
|
||||
&style.auth.cta_button,
|
||||
cx,
|
||||
|_, cx| {
|
||||
let window_id = cx.window_id();
|
||||
cx.remove_window(window_id);
|
||||
cx.platform().open_url(COPILOT_SIGN_UP_URL)
|
||||
},
|
||||
)
|
||||
.boxed(),
|
||||
])
|
||||
.with_style(unauthorized_style.warning.container),
|
||||
)
|
||||
.with_child(theme::ui::cta_button::<Self, _, _, _>(
|
||||
"Subscribe on GitHub",
|
||||
style.auth.content_width,
|
||||
&style.auth.cta_button,
|
||||
cx,
|
||||
|_, _, cx| {
|
||||
cx.remove_window();
|
||||
cx.platform().open_url(COPILOT_SIGN_UP_URL)
|
||||
},
|
||||
))
|
||||
.align_children_center()
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,32 +327,50 @@ impl View for CopilotCodeVerification {
|
||||
"CopilotCodeVerification"
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
|
||||
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut gpui::ViewContext<Self>) {
|
||||
fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
let style = cx.global::<Settings>().theme.clone();
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum ConnectModal {}
|
||||
|
||||
modal("Connect Copilot to Zed", &style.copilot.modal, cx, |cx| {
|
||||
Flex::column()
|
||||
.with_children([
|
||||
theme::ui::icon(&style.copilot.auth.header).boxed(),
|
||||
match &self.status {
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => Self::render_prompting_modal(&prompt, &style.copilot, cx),
|
||||
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
|
||||
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
|
||||
_ => Empty::new().boxed(),
|
||||
},
|
||||
])
|
||||
.align_children_center()
|
||||
.boxed()
|
||||
})
|
||||
let style = theme::current(cx).clone();
|
||||
|
||||
modal::<ConnectModal, _, _, _, _>(
|
||||
"Connect Copilot to Zed",
|
||||
&style.copilot.modal,
|
||||
cx,
|
||||
|cx| {
|
||||
Flex::column()
|
||||
.with_children([
|
||||
theme::ui::icon(&style.copilot.auth.header).into_any(),
|
||||
match &self.status {
|
||||
Status::SigningIn {
|
||||
prompt: Some(prompt),
|
||||
} => Self::render_prompting_modal(
|
||||
self.connect_clicked,
|
||||
&prompt,
|
||||
&style.copilot,
|
||||
cx,
|
||||
),
|
||||
Status::Unauthorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_unauthorized_modal(&style.copilot, cx)
|
||||
}
|
||||
Status::Authorized => {
|
||||
self.connect_clicked = false;
|
||||
Self::render_enabled_modal(&style.copilot, cx)
|
||||
}
|
||||
_ => Empty::new().into_any(),
|
||||
},
|
||||
])
|
||||
.align_children_center()
|
||||
},
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,20 @@ path = "src/copilot_button.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
assets = { path = "../assets" }
|
||||
copilot = { path = "../copilot" }
|
||||
editor = { path = "../editor" }
|
||||
fs = { path = "../fs" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0"
|
||||
smol = "1.2.5"
|
||||
futures = "0.3"
|
||||
anyhow.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
||||
@@ -1,85 +1,34 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use editor::Editor;
|
||||
use copilot::{Copilot, SignOut, Status};
|
||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
impl_internal_actions,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Element, ElementBox, Entity, MouseState, RenderContext, Subscription, View,
|
||||
ViewContext, ViewHandle,
|
||||
AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use settings::{settings_file::SettingsFile, Settings};
|
||||
use language::language_settings::{self, all_language_settings, AllLanguageSettings};
|
||||
use settings::{update_settings_file, SettingsStore};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use util::{paths, ResultExt};
|
||||
use workspace::{
|
||||
item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast,
|
||||
StatusItemView,
|
||||
create_and_open_local_file, item::ItemHandle,
|
||||
notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
|
||||
};
|
||||
|
||||
use copilot::{Copilot, Reinstall, SignIn, SignOut, Status};
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
const COPILOT_STARTING_TOAST_ID: usize = 1337;
|
||||
const COPILOT_ERROR_TOAST_ID: usize = 1338;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployCopilotMenu;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct ToggleCopilotForLanguage {
|
||||
language: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct ToggleCopilotGlobally;
|
||||
|
||||
// TODO: Make the other code path use `get_or_insert` logic for this modal
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployCopilotModal;
|
||||
|
||||
impl_internal_actions!(
|
||||
copilot,
|
||||
[
|
||||
DeployCopilotMenu,
|
||||
DeployCopilotModal,
|
||||
ToggleCopilotForLanguage,
|
||||
ToggleCopilotGlobally,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(CopilotButton::deploy_copilot_menu);
|
||||
cx.add_action(
|
||||
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
|
||||
let language = action.language.to_owned();
|
||||
|
||||
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
|
||||
|
||||
SettingsFile::update(cx, move |file_contents| {
|
||||
file_contents.languages.insert(
|
||||
language.to_owned(),
|
||||
settings::EditorSettings {
|
||||
copilot: Some((!current_langauge).into()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
|
||||
let copilot_on = cx.global::<Settings>().copilot_on(None);
|
||||
|
||||
SettingsFile::update(cx, move |file_contents| {
|
||||
file_contents.editor.copilot = Some((!copilot_on).into())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub struct CopilotButton {
|
||||
popup_menu: ViewHandle<ContextMenu>,
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<str>>,
|
||||
path: Option<Arc<Path>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl Entity for CopilotButton {
|
||||
@@ -91,35 +40,34 @@ impl View for CopilotButton {
|
||||
"CopilotButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
if !settings.enable_copilot_integration {
|
||||
return Empty::new().boxed();
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let all_language_settings = &all_language_settings(cx);
|
||||
if !all_language_settings.copilot.feature_enabled {
|
||||
return Empty::new().into_any();
|
||||
}
|
||||
|
||||
let theme = settings.theme.clone();
|
||||
let theme = theme::current(cx).clone();
|
||||
let active = self.popup_menu.read(cx).visible();
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return Empty::new().boxed();
|
||||
return Empty::new().into_any();
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
||||
|
||||
let view_id = cx.view_id();
|
||||
let enabled = self
|
||||
.editor_enabled
|
||||
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self>::new(0, cx, {
|
||||
MouseEventHandler::<Self, _>::new(0, cx, {
|
||||
let theme = theme.clone();
|
||||
let status = status.clone();
|
||||
move |state, _cx| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.panel_buttons
|
||||
.button
|
||||
.style_for(state, active);
|
||||
|
||||
Flex::row()
|
||||
@@ -141,88 +89,66 @@ impl View for CopilotButton {
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.aligned()
|
||||
.named("copilot-icon"),
|
||||
.into_any_named("copilot-icon"),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, {
|
||||
let status = status.clone();
|
||||
move |_, cx| match status {
|
||||
Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
|
||||
Status::Starting { ref task } => {
|
||||
cx.dispatch_action(workspace::Toast::new(
|
||||
COPILOT_STARTING_TOAST_ID,
|
||||
"Copilot is starting...",
|
||||
));
|
||||
let window_id = cx.window_id();
|
||||
let task = task.to_owned();
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
cx.update(|cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
let status = copilot.read(cx).status();
|
||||
match status {
|
||||
Status::Authorized => cx.dispatch_action_at(
|
||||
window_id,
|
||||
view_id,
|
||||
workspace::Toast::new(
|
||||
COPILOT_STARTING_TOAST_ID,
|
||||
"Copilot has started!",
|
||||
),
|
||||
),
|
||||
_ => {
|
||||
cx.dispatch_action_at(
|
||||
window_id,
|
||||
view_id,
|
||||
DismissToast::new(COPILOT_STARTING_TOAST_ID),
|
||||
);
|
||||
cx.dispatch_global_action(SignIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
move |_, this, cx| match status {
|
||||
Status::Authorized => this.deploy_copilot_menu(cx),
|
||||
Status::Error(ref e) => {
|
||||
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>()
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(
|
||||
COPILOT_ERROR_TOAST_ID,
|
||||
format!("Copilot can't be started: {}", e),
|
||||
)
|
||||
.on_click(
|
||||
"Reinstall Copilot",
|
||||
|cx| {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
copilot.reinstall(cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
|
||||
COPILOT_ERROR_TOAST_ID,
|
||||
format!("Copilot can't be started: {}", e),
|
||||
"Reinstall Copilot",
|
||||
Reinstall,
|
||||
)),
|
||||
_ => cx.dispatch_action(SignIn),
|
||||
_ => this.deploy_copilot_start_menu(cx),
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self, _>(
|
||||
.with_tooltip::<Self>(
|
||||
0,
|
||||
"GitHub Copilot".into(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.popup_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
.with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let button_view_id = cx.view_id();
|
||||
let menu = cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
let mut menu = ContextMenu::new(button_view_id, cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
});
|
||||
@@ -231,7 +157,7 @@ impl CopilotButton {
|
||||
|
||||
Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
|
||||
|
||||
cx.observe_global::<Settings, _>(move |_, cx| cx.notify())
|
||||
cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
@@ -239,62 +165,102 @@ impl CopilotButton {
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
path: None,
|
||||
fs,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
|
||||
let settings = cx.global::<Settings>();
|
||||
pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let mut menu_options = Vec::with_capacity(2);
|
||||
let fs = self.fs.clone();
|
||||
|
||||
let mut menu_options = Vec::with_capacity(6);
|
||||
menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
|
||||
initiate_sign_in(cx)
|
||||
}));
|
||||
menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
|
||||
hide_copilot(fs.clone(), cx)
|
||||
}));
|
||||
|
||||
if let Some(language) = &self.language {
|
||||
let language_enabled = settings.copilot_on(Some(language.as_ref()));
|
||||
self.popup_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::BottomRight,
|
||||
menu_options,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
menu_options.push(ContextMenuItem::item(
|
||||
pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let fs = self.fs.clone();
|
||||
let mut menu_options = Vec::with_capacity(8);
|
||||
|
||||
if let Some(language) = self.language.clone() {
|
||||
let fs = fs.clone();
|
||||
let language_enabled =
|
||||
language_settings::language_settings(Some(language.as_ref()), cx)
|
||||
.show_copilot_suggestions;
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
format!(
|
||||
"{} Copilot for {}",
|
||||
if language_enabled {
|
||||
"Disable"
|
||||
} else {
|
||||
"Enable"
|
||||
},
|
||||
"{} Suggestions for {}",
|
||||
if language_enabled { "Hide" } else { "Show" },
|
||||
language
|
||||
),
|
||||
ToggleCopilotForLanguage {
|
||||
language: language.to_owned(),
|
||||
move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
|
||||
));
|
||||
}
|
||||
|
||||
let settings = settings::get::<AllLanguageSettings>(cx);
|
||||
|
||||
if let Some(path) = self.path.as_ref() {
|
||||
let path_enabled = settings.copilot_enabled_for_path(path);
|
||||
let path = path.clone();
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
format!(
|
||||
"{} Suggestions for This Path",
|
||||
if path_enabled { "Hide" } else { "Show" }
|
||||
),
|
||||
move |cx| {
|
||||
if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
|
||||
let workspace = workspace.downgrade();
|
||||
cx.spawn(|_, cx| {
|
||||
configure_disabled_globs(
|
||||
workspace,
|
||||
path_enabled.then_some(path.clone()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let globally_enabled = cx.global::<Settings>().copilot_on(None);
|
||||
menu_options.push(ContextMenuItem::item(
|
||||
let globally_enabled = settings.copilot_enabled(None, None);
|
||||
menu_options.push(ContextMenuItem::handler(
|
||||
if globally_enabled {
|
||||
"Disable Copilot Globally"
|
||||
"Hide Suggestions for All Files"
|
||||
} else {
|
||||
"Enable Copilot Globally"
|
||||
"Show Suggestions for All Files"
|
||||
},
|
||||
ToggleCopilotGlobally,
|
||||
move |cx| toggle_copilot_globally(fs.clone(), cx),
|
||||
));
|
||||
|
||||
menu_options.push(ContextMenuItem::Separator);
|
||||
|
||||
let icon_style = settings.theme.copilot.out_link_icon.clone();
|
||||
menu_options.push(ContextMenuItem::element_item(
|
||||
Box::new(
|
||||
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
|
||||
Flex::row()
|
||||
.with_children([
|
||||
Label::new("Copilot Settings", style.label.clone()).boxed(),
|
||||
theme::ui::icon(icon_style.style_for(state, false)).boxed(),
|
||||
])
|
||||
.align_children_center()
|
||||
.boxed()
|
||||
},
|
||||
),
|
||||
let icon_style = theme::current(cx).copilot.out_link_icon.clone();
|
||||
menu_options.push(ContextMenuItem::action(
|
||||
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
|
||||
Flex::row()
|
||||
.with_child(Label::new("Copilot Settings", style.label.clone()))
|
||||
.with_child(theme::ui::icon(icon_style.style_for(state, false)))
|
||||
.align_children_center()
|
||||
.into_any()
|
||||
},
|
||||
OsOpen::new(COPILOT_SETTINGS_URL),
|
||||
));
|
||||
|
||||
menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
|
||||
menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
|
||||
|
||||
self.popup_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
@@ -308,18 +274,19 @@ impl CopilotButton {
|
||||
|
||||
pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let settings = cx.global::<Settings>();
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
|
||||
let language_name = snapshot
|
||||
.language_at(suggestion_anchor)
|
||||
.map(|language| language.name());
|
||||
let path = snapshot.file_at(suggestion_anchor).map(|file| file.path());
|
||||
|
||||
self.language = language_name.clone();
|
||||
|
||||
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
|
||||
self.editor_enabled = Some(
|
||||
all_language_settings(cx)
|
||||
.copilot_enabled(language_name.as_deref(), path.map(|p| p.as_ref())),
|
||||
);
|
||||
self.language = language_name;
|
||||
self.path = path.cloned();
|
||||
|
||||
cx.notify()
|
||||
}
|
||||
@@ -339,3 +306,129 @@ impl StatusItemView for CopilotButton {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
async fn configure_disabled_globs(
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
path_to_disable: Option<Arc<Path>>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let settings_editor = workspace
|
||||
.update(&mut cx, |_, cx| {
|
||||
create_and_open_local_file(&paths::SETTINGS, cx, || {
|
||||
settings::initial_user_settings_content(&assets::Assets)
|
||||
.as_ref()
|
||||
.into()
|
||||
})
|
||||
})?
|
||||
.await?
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
settings_editor.downgrade().update(&mut cx, |item, cx| {
|
||||
let text = item.buffer().read(cx).snapshot(cx).text();
|
||||
|
||||
let settings = cx.global::<SettingsStore>();
|
||||
let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
|
||||
let copilot = file.copilot.get_or_insert_with(Default::default);
|
||||
let globs = copilot.disabled_globs.get_or_insert_with(|| {
|
||||
settings
|
||||
.get::<AllLanguageSettings>(None)
|
||||
.copilot
|
||||
.disabled_globs
|
||||
.iter()
|
||||
.map(|glob| glob.glob().to_string())
|
||||
.collect()
|
||||
});
|
||||
|
||||
if let Some(path_to_disable) = &path_to_disable {
|
||||
globs.push(path_to_disable.to_string_lossy().into_owned());
|
||||
} else {
|
||||
globs.clear();
|
||||
}
|
||||
});
|
||||
|
||||
if !edits.is_empty() {
|
||||
item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_ranges(edits.iter().map(|e| e.0.clone()));
|
||||
});
|
||||
|
||||
// When *enabling* a path, don't actually perform an edit, just select the range.
|
||||
if path_to_disable.is_some() {
|
||||
item.edit(edits.iter().cloned(), cx);
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(None, None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_copilot_for_language(language: Arc<str>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
let show_copilot_suggestions = all_language_settings(cx).copilot_enabled(Some(&language), None);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.languages
|
||||
.entry(language)
|
||||
.or_default()
|
||||
.show_copilot_suggestions = Some(!show_copilot_suggestions);
|
||||
});
|
||||
}
|
||||
|
||||
fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
|
||||
file.features.get_or_insert(Default::default()).copilot = Some(false);
|
||||
});
|
||||
}
|
||||
|
||||
fn initiate_sign_in(cx: &mut WindowContext) {
|
||||
let Some(copilot) = Copilot::global(cx) else {
|
||||
return;
|
||||
};
|
||||
let status = copilot.read(cx).status();
|
||||
|
||||
match status {
|
||||
Status::Starting { task } => {
|
||||
let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() else {
|
||||
return;
|
||||
};
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.show_toast(
|
||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let workspace = workspace.downgrade();
|
||||
cx.spawn(|mut cx| async move {
|
||||
task.await;
|
||||
if let Some(copilot) = cx.read(Copilot::global) {
|
||||
workspace
|
||||
.update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
|
||||
Status::Authorized => workspace.show_toast(
|
||||
Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
|
||||
cx,
|
||||
),
|
||||
_ => {
|
||||
workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
_ => {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| copilot.sign_in(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,17 +17,17 @@ gpui = { path = "../gpui" }
|
||||
sqlez = { path = "../sqlez" }
|
||||
sqlez_macros = { path = "../sqlez_macros" }
|
||||
util = { path = "../util" }
|
||||
anyhow = "1.0.57"
|
||||
indoc = "1.0.4"
|
||||
async-trait = "0.1"
|
||||
lazy_static = "1.4.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
parking_lot = "0.11.1"
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
smol = "1.2"
|
||||
anyhow.workspace = true
|
||||
indoc.workspace = true
|
||||
async-trait.workspace = true
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
env_logger = "0.9.1"
|
||||
tempdir = { version = "0.3.7" }
|
||||
env_logger.workspace = true
|
||||
tempdir.workspace = true
|
||||
|
||||
@@ -9,24 +9,29 @@ path = "src/diagnostics.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
project = { path = "../project" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
postage = { workspace = true }
|
||||
|
||||
anyhow.workspace = true
|
||||
smallvec.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
unindent = "0.1"
|
||||
client = { path = "../client", features = ["test-support"] }
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
serde_json = { workspace = true }
|
||||
theme = { path = "../theme", features = ["test-support"] }
|
||||
|
||||
serde_json.workspace = true
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod items;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeMap, HashSet};
|
||||
use collections::{BTreeSet, HashSet};
|
||||
use editor::{
|
||||
diagnostic_block_renderer,
|
||||
display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
|
||||
@@ -10,35 +10,34 @@ use editor::{
|
||||
Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
|
||||
};
|
||||
use gpui::{
|
||||
actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,
|
||||
AppContext, Entity, ModelHandle, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
|
||||
ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
|
||||
SelectionGoal,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
borrow::Cow,
|
||||
cmp::Ordering,
|
||||
ops::Range,
|
||||
path::PathBuf,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ThemeSettings;
|
||||
use util::TryFutureExt;
|
||||
use workspace::{
|
||||
item::{Item, ItemEvent, ItemHandle},
|
||||
ItemNavHistory, Pane, Workspace,
|
||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
|
||||
ItemNavHistory, Pane, PaneBackdrop, ToolbarItemLocation, Workspace,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy]);
|
||||
|
||||
impl_internal_actions!(diagnostics, [Jump]);
|
||||
|
||||
const CONTEXT_LINE_COUNT: u32 = 1;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
@@ -55,7 +54,7 @@ struct ProjectDiagnosticsEditor {
|
||||
summary: DiagnosticSummary,
|
||||
excerpts: ModelHandle<MultiBuffer>,
|
||||
path_states: Vec<PathState>,
|
||||
paths_to_update: BTreeMap<ProjectPath, usize>,
|
||||
paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
|
||||
}
|
||||
|
||||
struct PathState {
|
||||
@@ -71,6 +70,7 @@ struct Jump {
|
||||
}
|
||||
|
||||
struct DiagnosticGroupState {
|
||||
language_server_id: LanguageServerId,
|
||||
primary_diagnostic: DiagnosticEntry<language::Anchor>,
|
||||
primary_excerpt_ix: usize,
|
||||
excerpts: Vec<ExcerptId>,
|
||||
@@ -87,16 +87,20 @@ impl View for ProjectDiagnosticsEditor {
|
||||
"ProjectDiagnosticsEditor"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if self.path_states.is_empty() {
|
||||
let theme = &cx.global::<Settings>().theme.project_diagnostics;
|
||||
Label::new("No problems in workspace", theme.empty_message.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.boxed()
|
||||
let theme = &theme::current(cx).project_diagnostics;
|
||||
PaneBackdrop::new(
|
||||
cx.view_id(),
|
||||
Label::new("No problems in workspace", theme.empty_message.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.into_any(),
|
||||
)
|
||||
.into_any()
|
||||
} else {
|
||||
ChildView::new(&self.editor, cx).boxed()
|
||||
ChildView::new(&self.editor, cx).into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +119,7 @@ impl View for ProjectDiagnosticsEditor {
|
||||
}),
|
||||
"summary": self.summary,
|
||||
"paths_to_update": self.paths_to_update.iter().map(|(path, server_id)|
|
||||
(path.path.to_string_lossy(), server_id)
|
||||
(path.path.to_string_lossy(), server_id.0)
|
||||
).collect::<Vec<_>>(),
|
||||
"paths_states": self.path_states.iter().map(|state|
|
||||
json!({
|
||||
@@ -148,7 +152,7 @@ impl ProjectDiagnosticsEditor {
|
||||
path,
|
||||
} => {
|
||||
this.paths_to_update
|
||||
.insert(path.clone(), *language_server_id);
|
||||
.insert((path.clone(), *language_server_id));
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
@@ -161,13 +165,18 @@ impl ProjectDiagnosticsEditor {
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor
|
||||
});
|
||||
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
|
||||
.detach();
|
||||
cx.subscribe(&editor, |this, _, event, cx| {
|
||||
cx.emit(event.clone());
|
||||
if event == &editor::Event::Focused && this.path_states.is_empty() {
|
||||
cx.focus_self()
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let project = project_handle.read(cx);
|
||||
let paths_to_update = project
|
||||
.diagnostic_summaries(cx)
|
||||
.map(|e| (e.0, e.1.language_server_id))
|
||||
.map(|(path, server_id, _)| (path, server_id))
|
||||
.collect();
|
||||
let summary = project.diagnostic_summary(cx);
|
||||
let mut this = Self {
|
||||
@@ -195,9 +204,13 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_excerpts(&mut self, language_server_id: Option<usize>, cx: &mut ViewContext<Self>) {
|
||||
fn update_excerpts(
|
||||
&mut self,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let mut paths = Vec::new();
|
||||
self.paths_to_update.retain(|path, server_id| {
|
||||
self.paths_to_update.retain(|(path, server_id)| {
|
||||
if language_server_id
|
||||
.map_or(true, |language_server_id| language_server_id == *server_id)
|
||||
{
|
||||
@@ -214,7 +227,9 @@ impl ProjectDiagnosticsEditor {
|
||||
let buffer = project
|
||||
.update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| this.populate_excerpts(path, buffer, cx))
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.populate_excerpts(path, language_server_id, buffer, cx)
|
||||
})?;
|
||||
}
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
}
|
||||
@@ -226,6 +241,7 @@ impl ProjectDiagnosticsEditor {
|
||||
fn populate_excerpts(
|
||||
&mut self,
|
||||
path: ProjectPath,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
@@ -264,9 +280,9 @@ impl ProjectDiagnosticsEditor {
|
||||
let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
|
||||
let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
|
||||
let mut new_groups = snapshot
|
||||
.diagnostic_groups()
|
||||
.diagnostic_groups(language_server_id)
|
||||
.into_iter()
|
||||
.filter(|group| {
|
||||
.filter(|(_, group)| {
|
||||
group.entries[group.primary_ix].diagnostic.severity
|
||||
<= DiagnosticSeverity::WARNING
|
||||
})
|
||||
@@ -278,12 +294,27 @@ impl ProjectDiagnosticsEditor {
|
||||
match (old_groups.peek(), new_groups.peek()) {
|
||||
(None, None) => break,
|
||||
(None, Some(_)) => to_insert = new_groups.next(),
|
||||
(Some(_), None) => to_remove = old_groups.next(),
|
||||
(Some((_, old_group)), Some(new_group)) => {
|
||||
(Some((_, old_group)), None) => {
|
||||
if language_server_id.map_or(true, |id| id == old_group.language_server_id)
|
||||
{
|
||||
to_remove = old_groups.next();
|
||||
} else {
|
||||
to_keep = old_groups.next();
|
||||
}
|
||||
}
|
||||
(Some((_, old_group)), Some((_, new_group))) => {
|
||||
let old_primary = &old_group.primary_diagnostic;
|
||||
let new_primary = &new_group.entries[new_group.primary_ix];
|
||||
match compare_diagnostics(old_primary, new_primary, &snapshot) {
|
||||
Ordering::Less => to_remove = old_groups.next(),
|
||||
Ordering::Less => {
|
||||
if language_server_id
|
||||
.map_or(true, |id| id == old_group.language_server_id)
|
||||
{
|
||||
to_remove = old_groups.next();
|
||||
} else {
|
||||
to_keep = old_groups.next();
|
||||
}
|
||||
}
|
||||
Ordering::Equal => {
|
||||
to_keep = old_groups.next();
|
||||
new_groups.next();
|
||||
@@ -293,8 +324,9 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(group) = to_insert {
|
||||
if let Some((language_server_id, group)) = to_insert {
|
||||
let mut group_state = DiagnosticGroupState {
|
||||
language_server_id,
|
||||
primary_diagnostic: group.entries[group.primary_ix].clone(),
|
||||
primary_excerpt_ix: 0,
|
||||
excerpts: Default::default(),
|
||||
@@ -505,16 +537,16 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
impl Item for ProjectDiagnosticsEditor {
|
||||
fn tab_content(
|
||||
fn tab_content<T: View>(
|
||||
&self,
|
||||
_detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
) -> AnyElement<T> {
|
||||
render_summary(
|
||||
&self.summary,
|
||||
&style.label.text,
|
||||
&cx.global::<Settings>().theme.project_diagnostics,
|
||||
&theme::current(cx).project_diagnostics,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -526,11 +558,20 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
false
|
||||
}
|
||||
|
||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, cx))
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
|
||||
Some("Project Diagnostics".into())
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
self.excerpts.read(cx).is_dirty(cx)
|
||||
}
|
||||
@@ -568,15 +609,6 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.git_diff_recalc(project, cx))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
Editor::to_item_events(event)
|
||||
}
|
||||
@@ -625,6 +657,14 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
Some("diagnostics")
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft { flex: None }
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
project: ModelHandle<Project>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
@@ -637,12 +677,12 @@ impl Item for ProjectDiagnosticsEditor {
|
||||
}
|
||||
|
||||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
let (message, highlights) = highlight_diagnostic_message(&diagnostic.message);
|
||||
let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message);
|
||||
Arc::new(move |cx| {
|
||||
let settings = cx.global::<Settings>();
|
||||
let settings = settings::get::<ThemeSettings>(cx);
|
||||
let theme = &settings.theme.editor;
|
||||
let style = theme.diagnostic_header.clone();
|
||||
let font_size = (style.text_scale_factor * settings.buffer_font_size).round();
|
||||
let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
|
||||
let icon_width = cx.em_width * style.icon_width_factor;
|
||||
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
|
||||
Svg::new("icons/circle_x_mark_12.svg")
|
||||
@@ -658,8 +698,17 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
.with_width(icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.boxed(),
|
||||
.with_margin_right(cx.gutter_padding),
|
||||
)
|
||||
.with_children(diagnostic.source.as_ref().map(|source| {
|
||||
Label::new(
|
||||
format!("{source}: "),
|
||||
style.source.label.clone().with_font_size(font_size),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.message.container)
|
||||
.aligned()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
message.clone(),
|
||||
@@ -668,47 +717,45 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
||||
.with_highlights(highlights.clone())
|
||||
.contained()
|
||||
.with_style(style.message.container)
|
||||
.with_margin_left(cx.gutter_padding)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
.with_children(diagnostic.code.clone().map(|code| {
|
||||
Label::new(code, style.code.text.clone().with_font_size(font_size))
|
||||
.contained()
|
||||
.with_style(style.code.container)
|
||||
.aligned()
|
||||
.boxed()
|
||||
}))
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.with_padding_left(cx.gutter_padding)
|
||||
.with_padding_right(cx.gutter_padding)
|
||||
.expanded()
|
||||
.named("diagnostic header")
|
||||
.into_any_named("diagnostic header")
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn render_summary(
|
||||
pub(crate) fn render_summary<T: View>(
|
||||
summary: &DiagnosticSummary,
|
||||
text_style: &TextStyle,
|
||||
theme: &theme::ProjectDiagnostics,
|
||||
) -> ElementBox {
|
||||
) -> AnyElement<T> {
|
||||
if summary.error_count == 0 && summary.warning_count == 0 {
|
||||
Label::new("No problems", text_style.clone()).boxed()
|
||||
Label::new("No problems", text_style.clone()).into_any()
|
||||
} else {
|
||||
let icon_width = theme.tab_icon_width;
|
||||
let icon_spacing = theme.tab_icon_spacing;
|
||||
let summary_spacing = theme.tab_summary_spacing;
|
||||
Flex::row()
|
||||
.with_children([
|
||||
.with_child(
|
||||
Svg::new("icons/circle_x_mark_12.svg")
|
||||
.with_color(text_style.color)
|
||||
.constrained()
|
||||
.with_width(icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(icon_spacing)
|
||||
.named("no-icon"),
|
||||
.with_margin_right(icon_spacing),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
summary.error_count.to_string(),
|
||||
LabelStyle {
|
||||
@@ -716,8 +763,9 @@ pub(crate) fn render_summary(
|
||||
highlight_text: None,
|
||||
},
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
.with_child(
|
||||
Svg::new("icons/triangle_exclamation_12.svg")
|
||||
.with_color(text_style.color)
|
||||
.constrained()
|
||||
@@ -725,8 +773,9 @@ pub(crate) fn render_summary(
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(summary_spacing)
|
||||
.with_margin_right(icon_spacing)
|
||||
.named("warn-icon"),
|
||||
.with_margin_right(icon_spacing),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
summary.warning_count.to_string(),
|
||||
LabelStyle {
|
||||
@@ -734,10 +783,9 @@ pub(crate) fn render_summary(
|
||||
highlight_text: None,
|
||||
},
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
])
|
||||
.boxed()
|
||||
.aligned(),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -766,52 +814,53 @@ mod tests {
|
||||
display_map::{BlockContext, TransformBlock},
|
||||
DisplayPoint,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{TestAppContext, WindowContext};
|
||||
use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped};
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use unindent::Unindent as _;
|
||||
use workspace::AppState;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_diagnostics(cx: &mut TestAppContext) {
|
||||
let app_state = cx.update(AppState::test);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"consts.rs": "
|
||||
const a: i32 = 'a';
|
||||
const b: i32 = c;
|
||||
"
|
||||
.unindent(),
|
||||
init_test(cx);
|
||||
|
||||
"main.rs": "
|
||||
fn main() {
|
||||
let x = vec![];
|
||||
let y = vec![];
|
||||
a(x);
|
||||
b(y);
|
||||
// comment 1
|
||||
// comment 2
|
||||
c(y);
|
||||
d(x);
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"consts.rs": "
|
||||
const a: i32 = 'a';
|
||||
const b: i32 = c;
|
||||
"
|
||||
.unindent(),
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
"main.rs": "
|
||||
fn main() {
|
||||
let x = vec![];
|
||||
let y = vec![];
|
||||
a(x);
|
||||
b(y);
|
||||
// comment 1
|
||||
// comment 2
|
||||
c(y);
|
||||
d(x);
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let language_server_id = LanguageServerId(0);
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create some diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
0,
|
||||
language_server_id,
|
||||
PathBuf::from("/test/main.rs"),
|
||||
None,
|
||||
vec![
|
||||
@@ -892,7 +941,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Open the project diagnostics view while there are already diagnostics.
|
||||
let view = cx.add_view(&workspace, |cx| {
|
||||
let view = cx.add_view(window_id, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
@@ -960,10 +1009,10 @@ mod tests {
|
||||
|
||||
// Diagnostics are added for another earlier path.
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_started(0, cx);
|
||||
project.disk_based_diagnostics_started(language_server_id, cx);
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
0,
|
||||
language_server_id,
|
||||
PathBuf::from("/test/consts.rs"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
@@ -980,7 +1029,7 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project.disk_based_diagnostics_finished(0, cx);
|
||||
project.disk_based_diagnostics_finished(language_server_id, cx);
|
||||
});
|
||||
|
||||
view.next_notification(cx).await;
|
||||
@@ -1060,10 +1109,10 @@ mod tests {
|
||||
|
||||
// Diagnostics are added to the first path
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_started(0, cx);
|
||||
project.disk_based_diagnostics_started(language_server_id, cx);
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
0,
|
||||
language_server_id,
|
||||
PathBuf::from("/test/consts.rs"),
|
||||
None,
|
||||
vec![
|
||||
@@ -1096,7 +1145,7 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project.disk_based_diagnostics_finished(0, cx);
|
||||
project.disk_based_diagnostics_finished(language_server_id, cx);
|
||||
});
|
||||
|
||||
view.next_notification(cx).await;
|
||||
@@ -1176,10 +1225,286 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut AppContext) -> Vec<(u32, String)> {
|
||||
let mut presenter = cx.build_presenter(editor.id(), 0., Default::default());
|
||||
let mut cx = presenter.build_layout_context(Default::default(), false, cx);
|
||||
cx.render(editor, |editor, cx| {
|
||||
#[gpui::test]
|
||||
async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"main.js": "
|
||||
a();
|
||||
b();
|
||||
c();
|
||||
d();
|
||||
e();
|
||||
".unindent()
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let server_id_1 = LanguageServerId(100);
|
||||
let server_id_2 = LanguageServerId(101);
|
||||
let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
let view = cx.add_view(window_id, |cx| {
|
||||
ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
// Two language servers start updating diagnostics
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_started(server_id_1, cx);
|
||||
project.disk_based_diagnostics_started(server_id_2, cx);
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_1,
|
||||
PathBuf::from("/test/main.js"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "error 1".to_string(),
|
||||
severity: DiagnosticSeverity::WARNING,
|
||||
is_primary: true,
|
||||
is_disk_based: true,
|
||||
group_id: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_2,
|
||||
PathBuf::from("/test/main.js"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "warning 1".to_string(),
|
||||
severity: DiagnosticSeverity::ERROR,
|
||||
is_primary: true,
|
||||
is_disk_based: true,
|
||||
group_id: 2,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// The first language server finishes
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_finished(server_id_1, cx);
|
||||
});
|
||||
|
||||
// Only the first language server's diagnostics are shown.
|
||||
cx.foreground().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
concat!(
|
||||
"\n", // filename
|
||||
"\n", // padding
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"a();\n", //
|
||||
"b();",
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// The second language server finishes
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_finished(server_id_2, cx);
|
||||
});
|
||||
|
||||
// Both language server's diagnostics are shown.
|
||||
cx.foreground().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
(6, "collapsed context".into()),
|
||||
(7, "diagnostic header".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
concat!(
|
||||
"\n", // filename
|
||||
"\n", // padding
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"a();\n", // location
|
||||
"b();\n", //
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"a();\n", // context
|
||||
"b();\n", //
|
||||
"c();", // context
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// Both language servers start updating diagnostics, and the first server finishes.
|
||||
project.update(cx, |project, cx| {
|
||||
project.disk_based_diagnostics_started(server_id_1, cx);
|
||||
project.disk_based_diagnostics_started(server_id_2, cx);
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_1,
|
||||
PathBuf::from("/test/main.js"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "warning 2".to_string(),
|
||||
severity: DiagnosticSeverity::WARNING,
|
||||
is_primary: true,
|
||||
is_disk_based: true,
|
||||
group_id: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_2,
|
||||
PathBuf::from("/test/main.rs"),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project.disk_based_diagnostics_finished(server_id_1, cx);
|
||||
});
|
||||
|
||||
// Only the first language server's diagnostics are updated.
|
||||
cx.foreground().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
(7, "collapsed context".into()),
|
||||
(8, "diagnostic header".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
concat!(
|
||||
"\n", // filename
|
||||
"\n", // padding
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"a();\n", // location
|
||||
"b();\n", //
|
||||
"c();\n", // context
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"b();\n", // context
|
||||
"c();\n", //
|
||||
"d();", // context
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// The second language server finishes.
|
||||
project.update(cx, |project, cx| {
|
||||
project
|
||||
.update_diagnostic_entries(
|
||||
server_id_2,
|
||||
PathBuf::from("/test/main.js"),
|
||||
None,
|
||||
vec![DiagnosticEntry {
|
||||
range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)),
|
||||
diagnostic: Diagnostic {
|
||||
message: "warning 2".to_string(),
|
||||
severity: DiagnosticSeverity::WARNING,
|
||||
is_primary: true,
|
||||
is_disk_based: true,
|
||||
group_id: 1,
|
||||
..Default::default()
|
||||
},
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
project.disk_based_diagnostics_finished(server_id_2, cx);
|
||||
});
|
||||
|
||||
// Both language servers' diagnostics are updated.
|
||||
cx.foreground().run_until_parked();
|
||||
view.update(cx, |view, cx| {
|
||||
assert_eq!(
|
||||
editor_blocks(&view.editor, cx),
|
||||
[
|
||||
(0, "path header block".into()),
|
||||
(2, "diagnostic header".into()),
|
||||
(7, "collapsed context".into()),
|
||||
(8, "diagnostic header".into()),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
view.editor.update(cx, |editor, cx| editor.display_text(cx)),
|
||||
concat!(
|
||||
"\n", // filename
|
||||
"\n", // padding
|
||||
// diagnostic group 1
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"b();\n", // location
|
||||
"c();\n", //
|
||||
"d();\n", // context
|
||||
"\n", // collapsed context
|
||||
// diagnostic group 2
|
||||
"\n", // primary message
|
||||
"\n", // padding
|
||||
"c();\n", // context
|
||||
"d();\n", //
|
||||
"e();", // context
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
client::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn editor_blocks(editor: &ViewHandle<Editor>, cx: &mut WindowContext) -> Vec<(u32, String)> {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
snapshot
|
||||
.blocks_in_range(0..snapshot.max_point().row())
|
||||
@@ -1187,7 +1512,7 @@ mod tests {
|
||||
let name = match block {
|
||||
TransformBlock::Custom(block) => block
|
||||
.render(&mut BlockContext {
|
||||
cx,
|
||||
view_context: cx,
|
||||
anchor_x: 0.,
|
||||
scroll_x: 0.,
|
||||
gutter_padding: 0.,
|
||||
|
||||
@@ -3,19 +3,20 @@ use editor::{Editor, GoToDiagnostic};
|
||||
use gpui::{
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
serde_json, AppContext, Entity, ModelHandle, RenderContext, Subscription, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
serde_json, AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::Diagnostic;
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use workspace::{item::ItemHandle, StatusItemView};
|
||||
use lsp::LanguageServerId;
|
||||
use workspace::{item::ItemHandle, StatusItemView, Workspace};
|
||||
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
|
||||
pub struct DiagnosticIndicator {
|
||||
summary: project::DiagnosticSummary,
|
||||
active_editor: Option<WeakViewHandle<Editor>>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
current_diagnostic: Option<Diagnostic>,
|
||||
in_progress_checks: HashSet<usize>,
|
||||
in_progress_checks: HashSet<LanguageServerId>,
|
||||
_observe_active_editor: Option<Subscription>,
|
||||
}
|
||||
|
||||
@@ -24,7 +25,8 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
impl DiagnosticIndicator {
|
||||
pub fn new(project: &ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||
let project = workspace.project();
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
project::Event::DiskBasedDiagnosticsStarted { language_server_id } => {
|
||||
this.in_progress_checks.insert(*language_server_id);
|
||||
@@ -45,6 +47,7 @@ impl DiagnosticIndicator {
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.collect(),
|
||||
active_editor: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
current_diagnostic: None,
|
||||
_observe_active_editor: None,
|
||||
}
|
||||
@@ -84,17 +87,16 @@ impl View for DiagnosticIndicator {
|
||||
"DiagnosticIndicator"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
enum Summary {}
|
||||
enum Message {}
|
||||
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
let in_progress = !self.in_progress_checks.is_empty();
|
||||
let mut element = Flex::row().with_child(
|
||||
MouseEventHandler::<Summary>::new(0, cx, |state, cx| {
|
||||
let style = cx
|
||||
.global::<Settings>()
|
||||
.theme
|
||||
MouseEventHandler::<Summary, _>::new(0, cx, |state, cx| {
|
||||
let theme = theme::current(cx);
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.diagnostic_summary
|
||||
@@ -102,23 +104,23 @@ impl View for DiagnosticIndicator {
|
||||
|
||||
let mut summary_row = Flex::row();
|
||||
if self.summary.error_count > 0 {
|
||||
summary_row.add_children([
|
||||
summary_row.add_child(
|
||||
Svg::new("icons/circle_x_mark_16.svg")
|
||||
.with_color(style.icon_color_error)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(style.icon_spacing)
|
||||
.named("error-icon"),
|
||||
.with_margin_right(style.icon_spacing),
|
||||
);
|
||||
summary_row.add_child(
|
||||
Label::new(self.summary.error_count.to_string(), style.text.clone())
|
||||
.aligned()
|
||||
.boxed(),
|
||||
]);
|
||||
.aligned(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.summary.warning_count > 0 {
|
||||
summary_row.add_children([
|
||||
summary_row.add_child(
|
||||
Svg::new("icons/triangle_exclamation_16.svg")
|
||||
.with_color(style.icon_color_warning)
|
||||
.constrained()
|
||||
@@ -130,12 +132,12 @@ impl View for DiagnosticIndicator {
|
||||
style.summary_spacing
|
||||
} else {
|
||||
0.
|
||||
})
|
||||
.named("warning-icon"),
|
||||
}),
|
||||
);
|
||||
summary_row.add_child(
|
||||
Label::new(self.summary.warning_count.to_string(), style.text.clone())
|
||||
.aligned()
|
||||
.boxed(),
|
||||
]);
|
||||
.aligned(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.summary.error_count == 0 && self.summary.warning_count == 0 {
|
||||
@@ -145,7 +147,7 @@ impl View for DiagnosticIndicator {
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.named("ok-icon"),
|
||||
.into_any_named("ok-icon"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -160,11 +162,16 @@ impl View for DiagnosticIndicator {
|
||||
} else {
|
||||
style.container_ok
|
||||
})
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| cx.dispatch_action(crate::Deploy))
|
||||
.with_tooltip::<Summary, _>(
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
|
||||
})
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Summary>(
|
||||
0,
|
||||
"Project Diagnostics".to_string(),
|
||||
Some(Box::new(crate::Deploy)),
|
||||
@@ -172,10 +179,10 @@ impl View for DiagnosticIndicator {
|
||||
cx,
|
||||
)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.into_any(),
|
||||
);
|
||||
|
||||
let style = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
let style = &theme::current(cx).workspace.status_bar;
|
||||
let item_spacing = style.item_spacing;
|
||||
|
||||
if in_progress {
|
||||
@@ -183,13 +190,12 @@ impl View for DiagnosticIndicator {
|
||||
Label::new("Checking…", style.diagnostic_message.default.text.clone())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(item_spacing)
|
||||
.boxed(),
|
||||
.with_margin_left(item_spacing),
|
||||
);
|
||||
} else if let Some(diagnostic) = &self.current_diagnostic {
|
||||
let message_style = style.diagnostic_message.clone();
|
||||
element.add_child(
|
||||
MouseEventHandler::<Message>::new(1, cx, |state, _| {
|
||||
MouseEventHandler::<Message, _>::new(1, cx, |state, _| {
|
||||
Label::new(
|
||||
diagnostic.message.split('\n').next().unwrap().to_string(),
|
||||
message_style.style_for(state, false).text.clone(),
|
||||
@@ -197,17 +203,15 @@ impl View for DiagnosticIndicator {
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(item_spacing)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(GoToDiagnostic)
|
||||
})
|
||||
.boxed(),
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.go_to_next_diagnostic(&Default::default(), cx)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
element.named("diagnostic indicator")
|
||||
element.into_any_named("diagnostic indicator")
|
||||
}
|
||||
|
||||
fn debug_json(&self, _: &gpui::AppContext) -> serde_json::Value {
|
||||
|
||||
@@ -6,7 +6,7 @@ use gpui::{
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
scene::{MouseDown, MouseDrag},
|
||||
AppContext, Element, ElementBox, EventContext, RenderContext, View, WeakViewHandle,
|
||||
AnyElement, Element, View, ViewContext, WeakViewHandle, WindowContext,
|
||||
};
|
||||
|
||||
const DEAD_ZONE: f32 = 4.;
|
||||
@@ -26,7 +26,7 @@ enum State<V: View> {
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
payload: Rc<dyn Any + 'static>,
|
||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
|
||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut ViewContext<V>) -> AnyElement<V>>,
|
||||
},
|
||||
Canceled,
|
||||
}
|
||||
@@ -111,7 +111,7 @@ impl<V: View> DragAndDrop<V> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
|
||||
pub fn drag_started(event: MouseDown, cx: &mut WindowContext) {
|
||||
cx.update_global(|this: &mut Self, _| {
|
||||
this.currently_dragged = Some(State::Down {
|
||||
region_offset: event.position - event.region.origin(),
|
||||
@@ -123,8 +123,8 @@ impl<V: View> DragAndDrop<V> {
|
||||
pub fn dragging<T: Any>(
|
||||
event: MouseDrag,
|
||||
payload: Rc<T>,
|
||||
cx: &mut EventContext,
|
||||
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
|
||||
cx: &mut WindowContext,
|
||||
render: Rc<impl 'static + Fn(&T, &mut ViewContext<V>) -> AnyElement<V>>,
|
||||
) {
|
||||
let window_id = cx.window_id();
|
||||
cx.update_global(|this: &mut Self, cx| {
|
||||
@@ -178,7 +178,7 @@ impl<V: View> DragAndDrop<V> {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
|
||||
pub fn render(cx: &mut ViewContext<V>) -> Option<AnyElement<V>> {
|
||||
enum DraggedElementHandler {}
|
||||
cx.global::<Self>()
|
||||
.currently_dragged
|
||||
@@ -199,23 +199,25 @@ impl<V: View> DragAndDrop<V> {
|
||||
return None;
|
||||
}
|
||||
|
||||
let position = position - region_offset;
|
||||
let position = (position - region_offset).round();
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||
render(payload, cx)
|
||||
})
|
||||
MouseEventHandler::<DraggedElementHandler, V>::new(
|
||||
0,
|
||||
cx,
|
||||
|_, cx| render(payload, cx),
|
||||
)
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
.on_up(MouseButton::Left, |_, _, cx| {
|
||||
cx.window_context().defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
this.finish_dragging(cx)
|
||||
});
|
||||
});
|
||||
cx.propagate_event();
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
.on_up_out(MouseButton::Left, |_, _, cx| {
|
||||
cx.window_context().defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
this.finish_dragging(cx)
|
||||
});
|
||||
@@ -225,43 +227,38 @@ impl<V: View> DragAndDrop<V> {
|
||||
.with_hoverable(false)
|
||||
.constrained()
|
||||
.with_width(region.width())
|
||||
.with_height(region.height())
|
||||
.boxed(),
|
||||
.with_height(region.height()),
|
||||
)
|
||||
.with_anchor_position(position)
|
||||
.boxed(),
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
State::Canceled => Some(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(0.)
|
||||
.with_height(0.)
|
||||
.boxed()
|
||||
MouseEventHandler::<DraggedElementHandler, V>::new(0, cx, |_, _| {
|
||||
Empty::new().constrained().with_width(0.).with_height(0.)
|
||||
})
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
.on_up(MouseButton::Left, |_, _, cx| {
|
||||
cx.window_context().defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this.currently_dragged = None;
|
||||
});
|
||||
});
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
.on_up_out(MouseButton::Left, |_, _, cx| {
|
||||
cx.window_context().defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this.currently_dragged = None;
|
||||
});
|
||||
});
|
||||
})
|
||||
.boxed(),
|
||||
.into_any(),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut AppContext) {
|
||||
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut WindowContext) {
|
||||
if let Some(State::Dragging {
|
||||
payload, window_id, ..
|
||||
}) = &self.currently_dragged
|
||||
@@ -274,13 +271,13 @@ impl<V: View> DragAndDrop<V> {
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_dragging(&mut self, cx: &mut AppContext) {
|
||||
fn finish_dragging(&mut self, cx: &mut WindowContext) {
|
||||
if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
|
||||
self.notify_containers_for_window(window_id, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut AppContext) {
|
||||
fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut WindowContext) {
|
||||
self.containers.retain(|container| {
|
||||
if let Some(container) = container.upgrade(cx) {
|
||||
if container.window_id() == window_id {
|
||||
@@ -294,35 +291,35 @@ impl<V: View> DragAndDrop<V> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Draggable {
|
||||
fn as_draggable<V: View, P: Any>(
|
||||
pub trait Draggable<V: View> {
|
||||
fn as_draggable<D: View, P: Any>(
|
||||
self,
|
||||
payload: P,
|
||||
render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
|
||||
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl<Tag> Draggable for MouseEventHandler<Tag> {
|
||||
fn as_draggable<V: View, P: Any>(
|
||||
impl<Tag, V: View> Draggable<V> for MouseEventHandler<Tag, V> {
|
||||
fn as_draggable<D: View, P: Any>(
|
||||
self,
|
||||
payload: P,
|
||||
render: impl 'static + Fn(&P, &mut RenderContext<V>) -> ElementBox,
|
||||
render: impl 'static + Fn(&P, &mut ViewContext<D>) -> AnyElement<D>,
|
||||
) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let payload = Rc::new(payload);
|
||||
let render = Rc::new(render);
|
||||
self.on_down(MouseButton::Left, move |e, cx| {
|
||||
self.on_down(MouseButton::Left, move |e, _, cx| {
|
||||
cx.propagate_event();
|
||||
DragAndDrop::<V>::drag_started(e, cx);
|
||||
DragAndDrop::<D>::drag_started(e, cx);
|
||||
})
|
||||
.on_drag(MouseButton::Left, move |e, cx| {
|
||||
.on_drag(MouseButton::Left, move |e, _, cx| {
|
||||
let payload = payload.clone();
|
||||
let render = render.clone();
|
||||
DragAndDrop::<V>::dragging(e, payload, cx, render)
|
||||
DragAndDrop::<D>::dragging(e, payload, cx, render)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ test-support = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
copilot = { path = "../copilot" }
|
||||
db = { path = "../db" }
|
||||
@@ -46,20 +47,22 @@ sqlez = { path = "../sqlez" }
|
||||
workspace = { path = "../workspace" }
|
||||
|
||||
aho-corasick = "0.7"
|
||||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
indoc = "1.0.4"
|
||||
itertools = "0.10"
|
||||
lazy_static = "1.4"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = "2.1.1"
|
||||
parking_lot = "0.11"
|
||||
postage = { workspace = true }
|
||||
rand = { version = "0.8.3", optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_derive = { workspace = true }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
lazy_static.workspace = true
|
||||
log.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
pulldown-cmark = { version = "0.9.2", default-features = false }
|
||||
rand = { workspace = true, optional = true }
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
tree-sitter-rust = { version = "*", optional = true }
|
||||
tree-sitter-html = { version = "*", optional = true }
|
||||
tree-sitter-javascript = { version = "*", optional = true }
|
||||
@@ -75,10 +78,11 @@ util = { path = "../util", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
ctor = "0.1"
|
||||
env_logger = "0.9"
|
||||
rand = "0.8"
|
||||
unindent = "0.1.7"
|
||||
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
rand.workspace = true
|
||||
unindent.workspace = true
|
||||
tree-sitter = "0.20"
|
||||
tree-sitter-rust = "0.20"
|
||||
tree-sitter-html = "0.19"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::EditorSettings;
|
||||
use gpui::{Entity, ModelContext};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use smol::Timer;
|
||||
use std::time::Duration;
|
||||
|
||||
pub struct BlinkManager {
|
||||
blink_interval: Duration,
|
||||
@@ -15,8 +15,8 @@ pub struct BlinkManager {
|
||||
|
||||
impl BlinkManager {
|
||||
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
|
||||
cx.observe_global::<Settings, _>(move |this, cx| {
|
||||
// Make sure we blink the cursors if the setting is re-enabled
|
||||
// Make sure we blink the cursors if the setting is re-enabled
|
||||
cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
||||
this.blink_cursors(this.blink_epoch, cx)
|
||||
})
|
||||
.detach();
|
||||
@@ -64,7 +64,7 @@ impl BlinkManager {
|
||||
}
|
||||
|
||||
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
|
||||
if cx.global::<Settings>().cursor_blink {
|
||||
if settings::get::<EditorSettings>(cx).cursor_blink {
|
||||
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
|
||||
self.visible = !self.visible;
|
||||
cx.notify();
|
||||
|
||||
@@ -13,8 +13,9 @@ use gpui::{
|
||||
fonts::{FontId, HighlightStyle},
|
||||
Entity, ModelContext, ModelHandle,
|
||||
};
|
||||
use language::{OffsetUtf16, Point, Subscription as BufferSubscription};
|
||||
use settings::Settings;
|
||||
use language::{
|
||||
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
|
||||
};
|
||||
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||
pub use suggestion_map::Suggestion;
|
||||
use suggestion_map::SuggestionMap;
|
||||
@@ -276,8 +277,7 @@ impl DisplayMap {
|
||||
.as_singleton()
|
||||
.and_then(|buffer| buffer.read(cx).language())
|
||||
.map(|language| language.name());
|
||||
|
||||
cx.global::<Settings>().tab_size(language_name.as_deref())
|
||||
language_settings(language_name.as_deref(), cx).tab_size
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -844,8 +844,12 @@ pub mod tests {
|
||||
use super::*;
|
||||
use crate::{movement, test::marked_display_snapshot};
|
||||
use gpui::{color::Color, elements::*, test::observe, AppContext};
|
||||
use language::{Buffer, Language, LanguageConfig, SelectionGoal};
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
|
||||
Buffer, Language, LanguageConfig, SelectionGoal,
|
||||
};
|
||||
use rand::{prelude::*, Rng};
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{env, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
@@ -882,9 +886,7 @@ pub mod tests {
|
||||
log::info!("wrap width: {:?}", wrap_width);
|
||||
|
||||
cx.update(|cx| {
|
||||
let mut settings = Settings::test(cx);
|
||||
settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
|
||||
cx.set_global(settings)
|
||||
init_test(cx, |s| s.defaults.tab_size = NonZeroU32::new(tab_size));
|
||||
});
|
||||
|
||||
let buffer = cx.update(|cx| {
|
||||
@@ -939,9 +941,11 @@ pub mod tests {
|
||||
tab_size = *tab_sizes.choose(&mut rng).unwrap();
|
||||
log::info!("setting tab size to {:?}", tab_size);
|
||||
cx.update(|cx| {
|
||||
let mut settings = Settings::test(cx);
|
||||
settings.editor_overrides.tab_size = NonZeroU32::new(tab_size);
|
||||
cx.set_global(settings)
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |s| {
|
||||
s.defaults.tab_size = NonZeroU32::new(tab_size);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
30..=44 => {
|
||||
@@ -973,7 +977,7 @@ pub mod tests {
|
||||
position,
|
||||
height,
|
||||
disposition,
|
||||
render: Arc::new(|_| Empty::new().boxed()),
|
||||
render: Arc::new(|_| Empty::new().into_any()),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1119,7 +1123,7 @@ pub mod tests {
|
||||
#[gpui::test(retries = 5)]
|
||||
fn test_soft_wraps(cx: &mut AppContext) {
|
||||
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
cx.foreground().forbid_parking();
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let font_cache = cx.font_cache();
|
||||
|
||||
@@ -1131,7 +1135,6 @@ pub mod tests {
|
||||
.unwrap();
|
||||
let font_size = 12.0;
|
||||
let wrap_width = Some(64.);
|
||||
cx.set_global(Settings::test(cx));
|
||||
|
||||
let text = "one two three four five\nsix seven eight";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
@@ -1211,7 +1214,8 @@ pub mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_text_chunks(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let text = sample_text(6, 6, 'a');
|
||||
let buffer = MultiBuffer::build_simple(&text, cx);
|
||||
let family_id = cx
|
||||
@@ -1225,6 +1229,7 @@ pub mod tests {
|
||||
let font_size = 14.0;
|
||||
let map =
|
||||
cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
vec![
|
||||
@@ -1289,11 +1294,8 @@ pub mod tests {
|
||||
.unwrap(),
|
||||
);
|
||||
language.set_theme(&theme);
|
||||
cx.update(|cx| {
|
||||
let mut settings = Settings::test(cx);
|
||||
settings.editor_defaults.tab_size = Some(2.try_into().unwrap());
|
||||
cx.set_global(settings);
|
||||
});
|
||||
|
||||
cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap())));
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
|
||||
@@ -1382,7 +1384,7 @@ pub mod tests {
|
||||
);
|
||||
language.set_theme(&theme);
|
||||
|
||||
cx.update(|cx| cx.set_global(Settings::test(cx)));
|
||||
cx.update(|cx| init_test(cx, |_| {}));
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
|
||||
@@ -1429,9 +1431,8 @@ pub mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
|
||||
cx.update(|cx| init_test(cx, |_| {}));
|
||||
|
||||
cx.update(|cx| cx.set_global(Settings::test(cx)));
|
||||
let theme = SyntaxTheme::new(vec![
|
||||
("operator".to_string(), Color::red().into()),
|
||||
("string".to_string(), Color::green().into()),
|
||||
@@ -1510,7 +1511,8 @@ pub mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_clip_point(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx, |_| {});
|
||||
|
||||
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
|
||||
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
|
||||
|
||||
@@ -1559,7 +1561,7 @@ pub mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_clip_at_line_ends(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx, |_| {});
|
||||
|
||||
fn assert(text: &str, cx: &mut gpui::AppContext) {
|
||||
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
|
||||
@@ -1578,7 +1580,8 @@ pub mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let font_cache = cx.font_cache();
|
||||
@@ -1639,7 +1642,8 @@ pub mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_max_point(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
|
||||
let font_cache = cx.font_cache();
|
||||
let family_id = font_cache
|
||||
@@ -1718,4 +1722,13 @@ pub mod tests {
|
||||
}
|
||||
chunks
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
|
||||
cx.foreground().forbid_parking();
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
language::init(cx);
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, f);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ use super::{
|
||||
wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
|
||||
TextHighlights,
|
||||
};
|
||||
use crate::{Anchor, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
|
||||
use collections::{Bound, HashMap, HashSet};
|
||||
use gpui::{fonts::HighlightStyle, ElementBox, RenderContext};
|
||||
use gpui::{fonts::HighlightStyle, AnyElement, ViewContext};
|
||||
use language::{BufferSnapshot, Chunk, Patch, Point};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
@@ -50,7 +50,7 @@ struct BlockRow(u32);
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
struct WrapRow(u32);
|
||||
|
||||
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> ElementBox>;
|
||||
pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
|
||||
|
||||
pub struct Block {
|
||||
id: BlockId,
|
||||
@@ -69,7 +69,7 @@ where
|
||||
pub position: P,
|
||||
pub height: u8,
|
||||
pub style: BlockStyle,
|
||||
pub render: Arc<dyn Fn(&mut BlockContext) -> ElementBox>,
|
||||
pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
|
||||
pub disposition: BlockDisposition,
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ pub enum BlockStyle {
|
||||
Sticky,
|
||||
}
|
||||
|
||||
pub struct BlockContext<'a, 'b> {
|
||||
pub cx: &'b mut RenderContext<'a, crate::Editor>,
|
||||
pub struct BlockContext<'a, 'b, 'c> {
|
||||
pub view_context: &'c mut ViewContext<'a, 'b, Editor>,
|
||||
pub anchor_x: f32,
|
||||
pub scroll_x: f32,
|
||||
pub gutter_width: f32,
|
||||
@@ -833,10 +833,7 @@ impl<'a> Iterator for BlockChunks<'a> {
|
||||
|
||||
return Some(Chunk {
|
||||
text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
|
||||
syntax_highlight_id: None,
|
||||
highlight_style: None,
|
||||
diagnostic_severity: None,
|
||||
is_unnecessary: false,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -932,22 +929,22 @@ impl BlockDisposition {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> Deref for BlockContext<'a, 'b> {
|
||||
type Target = RenderContext<'a, crate::Editor>;
|
||||
impl<'a, 'b, 'c> Deref for BlockContext<'a, 'b, 'c> {
|
||||
type Target = ViewContext<'a, 'b, Editor>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.cx
|
||||
self.view_context
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> DerefMut for BlockContext<'a, 'b> {
|
||||
impl DerefMut for BlockContext<'_, '_, '_> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.cx
|
||||
self.view_context
|
||||
}
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub fn render(&self, cx: &mut BlockContext) -> ElementBox {
|
||||
pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
|
||||
self.render.lock()(cx)
|
||||
}
|
||||
|
||||
@@ -996,7 +993,7 @@ mod tests {
|
||||
use crate::multi_buffer::MultiBuffer;
|
||||
use gpui::{elements::Empty, Element};
|
||||
use rand::prelude::*;
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use std::env;
|
||||
use util::RandomCharIter;
|
||||
|
||||
@@ -1016,7 +1013,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_basic_blocks(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
let family_id = cx
|
||||
.font_cache()
|
||||
@@ -1045,21 +1042,21 @@ mod tests {
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 0)),
|
||||
height: 1,
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Arc::new(|_| Empty::new().named("block 1")),
|
||||
render: Arc::new(|_| Empty::new().into_any_named("block 1")),
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 2)),
|
||||
height: 2,
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Arc::new(|_| Empty::new().named("block 2")),
|
||||
render: Arc::new(|_| Empty::new().into_any_named("block 2")),
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(3, 3)),
|
||||
height: 3,
|
||||
disposition: BlockDisposition::Below,
|
||||
render: Arc::new(|_| Empty::new().named("block 3")),
|
||||
render: Arc::new(|_| Empty::new().into_any_named("block 3")),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -1192,7 +1189,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
let family_id = cx
|
||||
.font_cache()
|
||||
@@ -1219,14 +1216,14 @@ mod tests {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 12)),
|
||||
disposition: BlockDisposition::Above,
|
||||
render: Arc::new(|_| Empty::new().named("block 1")),
|
||||
render: Arc::new(|_| Empty::new().into_any_named("block 1")),
|
||||
height: 1,
|
||||
},
|
||||
BlockProperties {
|
||||
style: BlockStyle::Fixed,
|
||||
position: buffer_snapshot.anchor_after(Point::new(1, 1)),
|
||||
disposition: BlockDisposition::Below,
|
||||
render: Arc::new(|_| Empty::new().named("block 2")),
|
||||
render: Arc::new(|_| Empty::new().into_any_named("block 2")),
|
||||
height: 1,
|
||||
},
|
||||
]);
|
||||
@@ -1242,7 +1239,7 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
@@ -1329,7 +1326,7 @@ mod tests {
|
||||
position,
|
||||
height,
|
||||
disposition,
|
||||
render: Arc::new(|_| Empty::new().boxed()),
|
||||
render: Arc::new(|_| Empty::new().into_any()),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1650,6 +1647,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
}
|
||||
|
||||
impl TransformBlock {
|
||||
fn as_custom(&self) -> Option<&Block> {
|
||||
match self {
|
||||
|
||||
@@ -1065,13 +1065,11 @@ impl<'a> Iterator for FoldChunks<'a> {
|
||||
self.output_offset += output_text.len();
|
||||
return Some(Chunk {
|
||||
text: output_text,
|
||||
syntax_highlight_id: None,
|
||||
highlight_style: self.ellipses_color.map(|color| HighlightStyle {
|
||||
color: Some(color),
|
||||
..Default::default()
|
||||
}),
|
||||
diagnostic_severity: None,
|
||||
is_unnecessary: false,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1206,7 +1204,7 @@ mod tests {
|
||||
use crate::{MultiBuffer, ToPoint};
|
||||
use collections::HashSet;
|
||||
use rand::prelude::*;
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use std::{cmp::Reverse, env, mem, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
use util::test::sample_text;
|
||||
@@ -1215,7 +1213,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_basic_folds(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
|
||||
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
@@ -1288,7 +1286,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_adjacent_folds(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
|
||||
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
@@ -1351,7 +1349,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
|
||||
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
@@ -1402,7 +1400,7 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
@@ -1678,6 +1676,10 @@ mod tests {
|
||||
assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
|
||||
let buffer = self.buffer.lock().clone();
|
||||
|
||||
@@ -531,10 +531,8 @@ impl<'a> Iterator for SuggestionChunks<'a> {
|
||||
if let Some(chunk) = chunks.next() {
|
||||
return Some(Chunk {
|
||||
text: chunk,
|
||||
syntax_highlight_id: None,
|
||||
highlight_style: self.highlight_style,
|
||||
diagnostic_severity: None,
|
||||
is_unnecessary: false,
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
self.suggestion_chunks = None;
|
||||
@@ -580,7 +578,7 @@ mod tests {
|
||||
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
|
||||
use gpui::AppContext;
|
||||
use rand::{prelude::StdRng, Rng};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
env,
|
||||
ops::{Bound, RangeBounds},
|
||||
@@ -633,7 +631,8 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
@@ -836,6 +835,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut AppContext) {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
}
|
||||
|
||||
impl SuggestionMap {
|
||||
pub fn randomly_mutate(
|
||||
&self,
|
||||
|
||||
@@ -268,6 +268,7 @@ impl TabSnapshot {
|
||||
tab_size: self.tab_size,
|
||||
chunk: Chunk {
|
||||
text: &SPACES[0..(to_next_stop as usize)],
|
||||
is_tab: true,
|
||||
..Default::default()
|
||||
},
|
||||
inside_leading_tab: to_next_stop > 0,
|
||||
@@ -545,6 +546,7 @@ impl<'a> Iterator for TabChunks<'a> {
|
||||
self.output_position = next_output_position;
|
||||
return Some(Chunk {
|
||||
text: &SPACES[..len as usize],
|
||||
is_tab: true,
|
||||
..self.chunk
|
||||
});
|
||||
}
|
||||
@@ -654,6 +656,56 @@ mod tests {
|
||||
assert_eq!(tab_snapshot.text(), input);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_marking_tabs(cx: &mut gpui::AppContext) {
|
||||
let input = "\t \thello";
|
||||
|
||||
let buffer = MultiBuffer::build_simple(&input, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
chunks(&tab_snapshot, TabPoint::zero()),
|
||||
vec![
|
||||
(" ".to_string(), true),
|
||||
(" ".to_string(), false),
|
||||
(" ".to_string(), true),
|
||||
("hello".to_string(), false),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
chunks(&tab_snapshot, TabPoint::new(0, 2)),
|
||||
vec![
|
||||
(" ".to_string(), true),
|
||||
(" ".to_string(), false),
|
||||
(" ".to_string(), true),
|
||||
("hello".to_string(), false),
|
||||
]
|
||||
);
|
||||
|
||||
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
|
||||
let mut chunks = Vec::new();
|
||||
let mut was_tab = false;
|
||||
let mut text = String::new();
|
||||
for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
|
||||
if chunk.is_tab != was_tab {
|
||||
if !text.is_empty() {
|
||||
chunks.push((mem::take(&mut text), was_tab));
|
||||
}
|
||||
was_tab = chunk.is_tab;
|
||||
}
|
||||
text.push_str(chunk.text);
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
chunks.push((text, was_tab));
|
||||
}
|
||||
chunks
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
|
||||
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
|
||||
|
||||
@@ -1043,16 +1043,16 @@ mod tests {
|
||||
};
|
||||
use gpui::test::observe;
|
||||
use rand::prelude::*;
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt;
|
||||
use std::{cmp, env, num::NonZeroU32};
|
||||
use text::Rope;
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
|
||||
cx.update(|cx| cx.set_global(Settings::test(cx)));
|
||||
init_test(cx);
|
||||
|
||||
cx.foreground().set_block_on_ticks(0..=50);
|
||||
cx.foreground().forbid_parking();
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
@@ -1287,6 +1287,14 @@ mod tests {
|
||||
wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
cx.update(|cx| {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn wrap_text(
|
||||
unwrapped_text: &str,
|
||||
wrap_width: Option<f32>,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
56
crates/editor/src/editor_settings.rs
Normal file
56
crates/editor/src/editor_settings.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct EditorSettings {
|
||||
pub cursor_blink: bool,
|
||||
pub hover_popover_enabled: bool,
|
||||
pub show_completions_on_input: bool,
|
||||
pub use_on_type_format: bool,
|
||||
pub scrollbar: Scrollbar,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct Scrollbar {
|
||||
pub show: ShowScrollbar,
|
||||
pub git_diff: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ShowScrollbar {
|
||||
Auto,
|
||||
System,
|
||||
Always,
|
||||
Never,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditorSettingsContent {
|
||||
pub cursor_blink: Option<bool>,
|
||||
pub hover_popover_enabled: Option<bool>,
|
||||
pub show_completions_on_input: Option<bool>,
|
||||
pub use_on_type_format: Option<bool>,
|
||||
pub scrollbar: Option<ScrollbarContent>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct ScrollbarContent {
|
||||
pub show: Option<ShowScrollbar>,
|
||||
pub git_diff: Option<bool>,
|
||||
}
|
||||
|
||||
impl Setting for EditorSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = EditorSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
use std::ops::RangeInclusive;
|
||||
use std::ops::Range;
|
||||
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use language::Point;
|
||||
@@ -15,7 +15,7 @@ pub enum DisplayDiffHunk {
|
||||
},
|
||||
|
||||
Unfolded {
|
||||
display_row_range: RangeInclusive<u32>,
|
||||
display_row_range: Range<u32>,
|
||||
status: DiffHunkStatus,
|
||||
},
|
||||
}
|
||||
@@ -26,7 +26,7 @@ impl DisplayDiffHunk {
|
||||
&DisplayDiffHunk::Folded { display_row } => display_row,
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => *display_row_range.start(),
|
||||
} => display_row_range.start,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ impl DisplayDiffHunk {
|
||||
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range, ..
|
||||
} => display_row_range.clone(),
|
||||
} => display_row_range.start..=display_row_range.end - 1,
|
||||
};
|
||||
|
||||
range.contains(&display_row)
|
||||
@@ -77,16 +77,12 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
|
||||
} else {
|
||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||
|
||||
let hunk_end_row_inclusive = hunk
|
||||
.buffer_range
|
||||
.end
|
||||
.saturating_sub(1)
|
||||
.max(hunk.buffer_range.start);
|
||||
let hunk_end_row_inclusive = hunk.buffer_range.end.max(hunk.buffer_range.start);
|
||||
let hunk_end_point = Point::new(hunk_end_row_inclusive, 0);
|
||||
let end = hunk_end_point.to_display_point(snapshot).row();
|
||||
|
||||
DisplayDiffHunk::Unfolded {
|
||||
display_row_range: start..=end,
|
||||
display_row_range: start..end,
|
||||
status: hunk.status(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,14 @@ pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewCon
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
use indoc::indoc;
|
||||
use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new(
|
||||
Language::new(
|
||||
LanguageConfig {
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings,
|
||||
EditorSnapshot, EditorStyle, RangeToAnchorExt,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, Text},
|
||||
impl_internal_actions,
|
||||
elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
|
||||
fonts::{HighlightStyle, Underline, Weight},
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Axis, Element, ElementBox, ModelHandle, RenderContext, Task, ViewContext,
|
||||
AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
|
||||
};
|
||||
use language::{Bias, DiagnosticEntry, DiagnosticSeverity};
|
||||
use project::{HoverBlock, Project};
|
||||
use settings::Settings;
|
||||
use std::{ops::Range, time::Duration};
|
||||
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
|
||||
use project::{HoverBlock, HoverBlockKind, Project};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use util::TryFutureExt;
|
||||
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSnapshot,
|
||||
EditorStyle, GoToDiagnostic, RangeToAnchorExt,
|
||||
};
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
@@ -24,21 +22,10 @@ pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
|
||||
pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
|
||||
pub const HOVER_POPOVER_GAP: f32 = 10.;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct HoverAt {
|
||||
pub point: Option<DisplayPoint>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub struct HideHover;
|
||||
|
||||
actions!(editor, [Hover]);
|
||||
impl_internal_actions!(editor, [HoverAt, HideHover]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
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
|
||||
@@ -49,12 +36,12 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
|
||||
|
||||
/// The internal hover action dispatches between `show_hover` or `hide_hover`
|
||||
/// depending on whether a point to hover over is provided.
|
||||
pub fn hover_at(editor: &mut Editor, action: &HoverAt, cx: &mut ViewContext<Editor>) {
|
||||
if cx.global::<Settings>().hover_popover_enabled {
|
||||
if let Some(point) = action.point {
|
||||
pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
|
||||
if settings::get::<EditorSettings>(cx).hover_popover_enabled {
|
||||
if let Some(point) = point {
|
||||
show_hover(editor, point, false, cx);
|
||||
} else {
|
||||
hide_hover(editor, &HideHover, cx);
|
||||
hide_hover(editor, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +49,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, _: &HideHover, cx: &mut ViewContext<Editor>) -> bool {
|
||||
pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let did_hide = editor.hover_state.info_popover.take().is_some()
|
||||
| editor.hover_state.diagnostic_popover.take().is_some();
|
||||
|
||||
@@ -129,7 +116,7 @@ fn show_hover(
|
||||
// Hover triggered from same location as last time. Don't show again.
|
||||
return;
|
||||
} else {
|
||||
hide_hover(editor, &HideHover, cx);
|
||||
hide_hover(editor, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,7 +136,7 @@ fn show_hover(
|
||||
}
|
||||
}
|
||||
|
||||
let task = cx.spawn_weak(|this, mut cx| {
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
// If we need to delay, delay a set amount initially before making the lsp request
|
||||
let delay = if !ignore_timeout {
|
||||
@@ -201,15 +188,13 @@ fn show_hover(
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.hover_state.diagnostic_popover =
|
||||
local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
|
||||
local_diagnostic,
|
||||
primary_diagnostic,
|
||||
});
|
||||
});
|
||||
}
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.hover_state.diagnostic_popover =
|
||||
local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
|
||||
local_diagnostic,
|
||||
primary_diagnostic,
|
||||
});
|
||||
})?;
|
||||
|
||||
// Construct new hover popover from hover request
|
||||
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
|
||||
@@ -235,27 +220,27 @@ fn show_hover(
|
||||
Some(InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: range,
|
||||
contents: hover_result.contents,
|
||||
blocks: hover_result.contents,
|
||||
rendered_content: None,
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(hover_popover) = hover_popover.as_ref() {
|
||||
// Highlight the selected symbol using a background highlight
|
||||
this.highlight_background::<HoverState>(
|
||||
vec![hover_popover.symbol_range.clone()],
|
||||
|theme| theme.editor.hover_popover.highlight,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.clear_background_highlights::<HoverState>(cx);
|
||||
}
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(hover_popover) = hover_popover.as_ref() {
|
||||
// Highlight the selected symbol using a background highlight
|
||||
this.highlight_background::<HoverState>(
|
||||
vec![hover_popover.symbol_range.clone()],
|
||||
|theme| theme.editor.hover_popover.highlight,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
this.clear_background_highlights::<HoverState>(cx);
|
||||
}
|
||||
|
||||
this.hover_state.info_popover = hover_popover;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
this.hover_state.info_popover = hover_popover;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
.log_err()
|
||||
@@ -264,6 +249,225 @@ fn show_hover(
|
||||
editor.hover_state.info_task = Some(task);
|
||||
}
|
||||
|
||||
fn render_blocks(
|
||||
theme_id: usize,
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
style: &EditorStyle,
|
||||
) -> RenderedInfo {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
|
||||
for block in blocks {
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
HoverBlockKind::Markdown => {
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
|
||||
|
||||
let mut bold_depth = 0;
|
||||
let mut italic_depth = 0;
|
||||
let mut link_url = None;
|
||||
let mut current_language = None;
|
||||
let mut list_stack = Vec::new();
|
||||
|
||||
for event in Parser::new_ext(&block.text, Options::all()) {
|
||||
let prev_len = text.len();
|
||||
match event {
|
||||
Event::Text(t) => {
|
||||
if let Some(language) = ¤t_language {
|
||||
render_code(
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
t.as_ref(),
|
||||
language,
|
||||
style,
|
||||
);
|
||||
} else {
|
||||
text.push_str(t.as_ref());
|
||||
|
||||
let mut style = HighlightStyle::default();
|
||||
if bold_depth > 0 {
|
||||
style.weight = Some(Weight::BOLD);
|
||||
}
|
||||
if italic_depth > 0 {
|
||||
style.italic = Some(true);
|
||||
}
|
||||
if let Some(link_url) = link_url.clone() {
|
||||
region_ranges.push(prev_len..text.len());
|
||||
regions.push(RenderedRegion {
|
||||
link_url: Some(link_url),
|
||||
code: false,
|
||||
});
|
||||
style.underline = Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
if style != HighlightStyle::default() {
|
||||
let mut new_highlight = true;
|
||||
if let Some((last_range, last_style)) = highlights.last_mut() {
|
||||
if last_range.end == prev_len && last_style == &style {
|
||||
last_range.end = text.len();
|
||||
new_highlight = false;
|
||||
}
|
||||
}
|
||||
if new_highlight {
|
||||
highlights.push((prev_len..text.len(), style));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Code(t) => {
|
||||
text.push_str(t.as_ref());
|
||||
region_ranges.push(prev_len..text.len());
|
||||
if link_url.is_some() {
|
||||
highlights.push((
|
||||
prev_len..text.len(),
|
||||
HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
regions.push(RenderedRegion {
|
||||
code: true,
|
||||
link_url: link_url.clone(),
|
||||
});
|
||||
}
|
||||
Event::Start(tag) => match tag {
|
||||
Tag::Paragraph => new_paragraph(&mut text, &mut list_stack),
|
||||
Tag::Heading(_, _, _) => {
|
||||
new_paragraph(&mut text, &mut list_stack);
|
||||
bold_depth += 1;
|
||||
}
|
||||
Tag::CodeBlock(kind) => {
|
||||
new_paragraph(&mut text, &mut list_stack);
|
||||
if let CodeBlockKind::Fenced(language) = kind {
|
||||
current_language = language_registry
|
||||
.language_for_name(language.as_ref())
|
||||
.now_or_never()
|
||||
.and_then(Result::ok);
|
||||
}
|
||||
}
|
||||
Tag::Emphasis => italic_depth += 1,
|
||||
Tag::Strong => bold_depth += 1,
|
||||
Tag::Link(_, url, _) => link_url = Some(url.to_string()),
|
||||
Tag::List(number) => {
|
||||
list_stack.push((number, false));
|
||||
}
|
||||
Tag::Item => {
|
||||
let len = list_stack.len();
|
||||
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
||||
*has_content = false;
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..len - 1 {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if let Some(number) = list_number {
|
||||
text.push_str(&format!("{}. ", number));
|
||||
*number += 1;
|
||||
*has_content = false;
|
||||
} else {
|
||||
text.push_str("- ");
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Event::End(tag) => match tag {
|
||||
Tag::Heading(_, _, _) => bold_depth -= 1,
|
||||
Tag::CodeBlock(_) => current_language = None,
|
||||
Tag::Emphasis => italic_depth -= 1,
|
||||
Tag::Strong => bold_depth -= 1,
|
||||
Tag::Link(_, _, _) => link_url = None,
|
||||
Tag::List(_) => drop(list_stack.pop()),
|
||||
_ => {}
|
||||
},
|
||||
Event::HardBreak => text.push('\n'),
|
||||
Event::SoftBreak => text.push(' '),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
HoverBlockKind::Code { language } => {
|
||||
if let Some(language) = language_registry
|
||||
.language_for_name(language)
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
{
|
||||
render_code(&mut text, &mut highlights, &block.text, &language, style);
|
||||
} else {
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() && !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
|
||||
RenderedInfo {
|
||||
theme_id,
|
||||
text,
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_code(
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
|
||||
content: &str,
|
||||
language: &Arc<Language>,
|
||||
style: &EditorStyle,
|
||||
) {
|
||||
let prev_len = text.len();
|
||||
text.push_str(content);
|
||||
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
|
||||
if let Some(style) = highlight_id.style(&style.syntax) {
|
||||
highlights.push((prev_len + range.start..prev_len + range.end, style));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
|
||||
let mut is_subsequent_paragraph_of_list = false;
|
||||
if let Some((_, has_content)) = list_stack.last_mut() {
|
||||
if *has_content {
|
||||
is_subsequent_paragraph_of_list = true;
|
||||
} else {
|
||||
*has_content = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !text.is_empty() {
|
||||
if !text.ends_with('\n') {
|
||||
text.push('\n');
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
for _ in 0..list_stack.len().saturating_sub(1) {
|
||||
text.push_str(" ");
|
||||
}
|
||||
if is_subsequent_paragraph_of_list {
|
||||
text.push_str(" ");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HoverState {
|
||||
pub info_popover: Option<InfoPopover>,
|
||||
@@ -278,12 +482,12 @@ impl HoverState {
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
&mut self,
|
||||
snapshot: &EditorSnapshot,
|
||||
style: &EditorStyle,
|
||||
visible_rows: Range<u32>,
|
||||
cx: &mut RenderContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<ElementBox>)> {
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
|
||||
// If there is a diagnostic, position the popovers based on that.
|
||||
// Otherwise use the start of the hover range
|
||||
let anchor = self
|
||||
@@ -307,7 +511,7 @@ impl HoverState {
|
||||
if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
|
||||
elements.push(diagnostic_popover.render(style, cx));
|
||||
}
|
||||
if let Some(info_popover) = self.info_popover.as_ref() {
|
||||
if let Some(info_popover) = self.info_popover.as_mut() {
|
||||
elements.push(info_popover.render(style, cx));
|
||||
}
|
||||
|
||||
@@ -319,55 +523,101 @@ impl HoverState {
|
||||
pub struct InfoPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
pub symbol_range: Range<Anchor>,
|
||||
pub contents: Vec<HoverBlock>,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
rendered_content: Option<RenderedInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RenderedInfo {
|
||||
theme_id: usize,
|
||||
text: String,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
region_ranges: Vec<Range<usize>>,
|
||||
regions: Vec<RenderedRegion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct RenderedRegion {
|
||||
code: bool,
|
||||
link_url: Option<String>,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
|
||||
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 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());
|
||||
pub fn render(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement<Editor> {
|
||||
if let Some(rendered) = &self.rendered_content {
|
||||
if rendered.theme_id != style.theme_id {
|
||||
self.rendered_content = None;
|
||||
}
|
||||
}
|
||||
|
||||
Text::new(content.text.clone(), style.text.clone())
|
||||
.with_soft_wrap(true)
|
||||
.with_highlights(
|
||||
runs.iter()
|
||||
.filter_map(|(range, id)| {
|
||||
id.style(style.theme.syntax.as_ref())
|
||||
.map(|style| (range.clone(), style))
|
||||
})
|
||||
.collect(),
|
||||
let rendered_content = self.rendered_content.get_or_insert_with(|| {
|
||||
render_blocks(
|
||||
style.theme_id,
|
||||
&self.blocks,
|
||||
self.project.read(cx).languages(),
|
||||
style,
|
||||
)
|
||||
});
|
||||
|
||||
MouseEventHandler::<InfoPopover, _>::new(0, cx, |_, cx| {
|
||||
let mut region_id = 0;
|
||||
let view_id = cx.view_id();
|
||||
|
||||
let code_span_background_color = style.document_highlight_read_background;
|
||||
let regions = rendered_content.regions.clone();
|
||||
Flex::column()
|
||||
.scrollable::<HoverBlock>(1, None, cx)
|
||||
.with_child(
|
||||
Text::new(rendered_content.text.clone(), style.text.clone())
|
||||
.with_highlights(rendered_content.highlights.clone())
|
||||
.with_custom_runs(
|
||||
rendered_content.region_ranges.clone(),
|
||||
move |ix, bounds, scene, _| {
|
||||
region_id += 1;
|
||||
let region = regions[ix].clone();
|
||||
if let Some(url) = region.link_url {
|
||||
scene.push_cursor_region(CursorRegion {
|
||||
bounds,
|
||||
style: CursorStyle::PointingHand,
|
||||
});
|
||||
scene.push_mouse_region(
|
||||
MouseRegion::new::<Self>(view_id, region_id, bounds)
|
||||
.on_click::<Editor, _>(
|
||||
MouseButton::Left,
|
||||
move |_, _, cx| {
|
||||
println!("clicked link {url}");
|
||||
cx.platform().open_url(&url);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if region.code {
|
||||
scene.push_quad(gpui::Quad {
|
||||
bounds,
|
||||
background: Some(code_span_background_color),
|
||||
border: Default::default(),
|
||||
corner_radius: 2.0,
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.boxed()
|
||||
} else {
|
||||
let mut text_style = style.hover_popover.prose.clone();
|
||||
text_style.font_size = style.text.font_size;
|
||||
|
||||
Text::new(content.text.clone(), text_style)
|
||||
.with_soft_wrap(true)
|
||||
.contained()
|
||||
.with_style(style.hover_popover.block_style)
|
||||
.boxed()
|
||||
}
|
||||
}));
|
||||
flex.contained()
|
||||
.with_soft_wrap(true),
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.hover_popover.container)
|
||||
.boxed()
|
||||
})
|
||||
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
|
||||
.on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.with_padding(Padding {
|
||||
bottom: HOVER_POPOVER_GAP,
|
||||
top: HOVER_POPOVER_GAP,
|
||||
..Default::default()
|
||||
})
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,11 +628,22 @@ pub struct DiagnosticPopover {
|
||||
}
|
||||
|
||||
impl DiagnosticPopover {
|
||||
pub fn render(&self, style: &EditorStyle, cx: &mut RenderContext<Editor>) -> ElementBox {
|
||||
pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
|
||||
enum PrimaryDiagnostic {}
|
||||
|
||||
let mut text_style = style.hover_popover.prose.clone();
|
||||
text_style.font_size = style.text.font_size;
|
||||
let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
|
||||
|
||||
let text = match &self.local_diagnostic.diagnostic.source {
|
||||
Some(source) => Text::new(
|
||||
format!("{source}: {}", self.local_diagnostic.diagnostic.message),
|
||||
text_style,
|
||||
)
|
||||
.with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
|
||||
|
||||
None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
|
||||
};
|
||||
|
||||
let container_style = match self.local_diagnostic.diagnostic.severity {
|
||||
DiagnosticSeverity::HINT => style.hover_popover.info_container,
|
||||
@@ -392,33 +653,31 @@ impl DiagnosticPopover {
|
||||
_ => style.hover_popover.container,
|
||||
};
|
||||
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
let tooltip_style = theme::current(cx).tooltip.clone();
|
||||
|
||||
MouseEventHandler::<DiagnosticPopover>::new(0, cx, |_, _| {
|
||||
Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style)
|
||||
.with_soft_wrap(true)
|
||||
MouseEventHandler::<DiagnosticPopover, _>::new(0, cx, |_, _| {
|
||||
text.with_soft_wrap(true)
|
||||
.contained()
|
||||
.with_style(container_style)
|
||||
.boxed()
|
||||
})
|
||||
.with_padding(Padding {
|
||||
top: HOVER_POPOVER_GAP,
|
||||
bottom: HOVER_POPOVER_GAP,
|
||||
..Default::default()
|
||||
})
|
||||
.on_move(|_, _| {}) // Consume move events so they don't reach regions underneath.
|
||||
.on_click(MouseButton::Left, |_, cx| {
|
||||
cx.dispatch_action(GoToDiagnostic)
|
||||
.on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
|
||||
.on_click(MouseButton::Left, |_, this, cx| {
|
||||
this.go_to_diagnostic(&Default::default(), cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_tooltip::<PrimaryDiagnostic, _>(
|
||||
.with_tooltip::<PrimaryDiagnostic>(
|
||||
0,
|
||||
"Go To Diagnostic".to_string(),
|
||||
Some(Box::new(crate::GoToDiagnostic)),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.boxed()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub fn activation_info(&self) -> (usize, Anchor) {
|
||||
@@ -433,18 +692,21 @@ impl DiagnosticPopover {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::indoc;
|
||||
|
||||
use language::{Diagnostic, DiagnosticSet};
|
||||
use project::HoverBlock;
|
||||
use smol::stream::StreamExt;
|
||||
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
use gpui::fonts::Weight;
|
||||
use indoc::indoc;
|
||||
use language::{Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use smol::stream::StreamExt;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
@@ -462,15 +724,7 @@ mod tests {
|
||||
fn test() { printˇln!(); }
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
hover_at(
|
||||
editor,
|
||||
&HoverAt {
|
||||
point: Some(hover_point),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
|
||||
assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
|
||||
|
||||
// After delay, hover should be visible.
|
||||
@@ -482,10 +736,7 @@ mod tests {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some basic docs
|
||||
Some test documentation"}
|
||||
.to_string(),
|
||||
value: "some basic docs".to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
@@ -497,17 +748,11 @@ mod tests {
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popover.clone().unwrap().contents,
|
||||
vec![
|
||||
HoverBlock {
|
||||
text: "Some basic docs".to_string(),
|
||||
language: None
|
||||
},
|
||||
HoverBlock {
|
||||
text: "Some test documentation".to_string(),
|
||||
language: None
|
||||
}
|
||||
]
|
||||
editor.hover_state.info_popover.clone().unwrap().blocks,
|
||||
vec![HoverBlock {
|
||||
text: "some basic docs".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
},]
|
||||
)
|
||||
});
|
||||
|
||||
@@ -518,15 +763,7 @@ mod tests {
|
||||
let mut request = cx
|
||||
.lsp
|
||||
.handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
|
||||
cx.update_editor(|editor, cx| {
|
||||
hover_at(
|
||||
editor,
|
||||
&HoverAt {
|
||||
point: Some(hover_point),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
|
||||
cx.foreground()
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
request.next().await;
|
||||
@@ -537,6 +774,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
@@ -558,10 +797,7 @@ mod tests {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some other basic docs
|
||||
Some other test documentation"}
|
||||
.to_string(),
|
||||
value: "some other basic docs".to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
@@ -572,23 +808,19 @@ mod tests {
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popover.clone().unwrap().contents,
|
||||
vec![
|
||||
HoverBlock {
|
||||
text: "Some other basic docs".to_string(),
|
||||
language: None
|
||||
},
|
||||
HoverBlock {
|
||||
text: "Some other test documentation".to_string(),
|
||||
language: None
|
||||
}
|
||||
]
|
||||
editor.hover_state.info_popover.clone().unwrap().blocks,
|
||||
vec![HoverBlock {
|
||||
text: "some other basic docs".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}]
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
@@ -620,7 +852,7 @@ mod tests {
|
||||
}],
|
||||
&snapshot,
|
||||
);
|
||||
buffer.update_diagnostics(set, cx);
|
||||
buffer.update_diagnostics(LanguageServerId(0), set, cx);
|
||||
});
|
||||
|
||||
// Hover pops diagnostic immediately
|
||||
@@ -639,10 +871,7 @@ mod tests {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: indoc! {"
|
||||
# Some other basic docs
|
||||
Some other test documentation"}
|
||||
.to_string(),
|
||||
value: "some new docs".to_string(),
|
||||
}),
|
||||
range: Some(range),
|
||||
}))
|
||||
@@ -655,4 +884,144 @@ mod tests {
|
||||
hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.add_window(|cx| {
|
||||
let editor = Editor::single_line(None, cx);
|
||||
let style = editor.style(cx);
|
||||
|
||||
struct Row {
|
||||
blocks: Vec<HoverBlock>,
|
||||
expected_marked_text: String,
|
||||
expected_styles: Vec<HighlightStyle>,
|
||||
}
|
||||
|
||||
let rows = &[
|
||||
// Strong emphasis
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "one **two** three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three\n".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
weight: Some(Weight::BOLD),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Links
|
||||
Row {
|
||||
blocks: vec three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three\n".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Lists
|
||||
Row {
|
||||
blocks: vec
|
||||
- d
|
||||
"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
lists:
|
||||
- one
|
||||
- a
|
||||
- b
|
||||
- two
|
||||
- «c»
|
||||
- d
|
||||
"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Multi-paragraph list items
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "
|
||||
* one two
|
||||
three
|
||||
|
||||
* four five
|
||||
* six seven
|
||||
eight
|
||||
|
||||
nine
|
||||
* ten
|
||||
* six
|
||||
"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
- one two three
|
||||
- four five
|
||||
- six seven eight
|
||||
|
||||
nine
|
||||
- ten
|
||||
- six
|
||||
"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(Underline {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
for Row {
|
||||
blocks,
|
||||
expected_marked_text,
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = render_blocks(0, &blocks, &Default::default(), &style);
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
.into_iter()
|
||||
.zip(expected_styles.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
rendered.text, expected_text,
|
||||
"wrong text for input {blocks:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
rendered.highlights, expected_highlights,
|
||||
"wrong highlights for input {blocks:?}"
|
||||
);
|
||||
}
|
||||
|
||||
editor
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ use crate::{
|
||||
movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
|
||||
Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use anyhow::{Context, Result};
|
||||
use collections::HashSet;
|
||||
use futures::future::try_join_all;
|
||||
use gpui::{
|
||||
elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, RenderContext,
|
||||
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
|
||||
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use language::{
|
||||
@@ -16,7 +16,6 @@ use language::{
|
||||
};
|
||||
use project::{FormatTrigger, Item as _, Project, ProjectPath};
|
||||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
@@ -27,8 +26,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use text::Selection;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::item::FollowableItemHandle;
|
||||
use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
|
||||
use workspace::item::{BreadcrumbText, FollowableItemHandle};
|
||||
use workspace::{
|
||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
@@ -67,20 +66,23 @@ impl FollowableItem for Editor {
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let pane = pane.downgrade();
|
||||
Some(cx.spawn(|mut cx| async move {
|
||||
let mut buffers = futures::future::try_join_all(buffers).await?;
|
||||
let editor = pane.read_with(&cx, |pane, cx| {
|
||||
let mut editors = pane.items_of_type::<Self>();
|
||||
editors.find(|editor| {
|
||||
editor.remote_id(&client, cx) == Some(remote_id)
|
||||
|| state.singleton
|
||||
&& buffers.len() == 1
|
||||
&& editor.read(cx).buffer.read(cx).as_singleton().as_ref()
|
||||
== Some(&buffers[0])
|
||||
let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
|
||||
let singleton_buffer_matches = state.singleton
|
||||
&& buffers.first()
|
||||
== editor.read(cx).buffer.read(cx).as_singleton().as_ref();
|
||||
ids_match || singleton_buffer_matches
|
||||
})
|
||||
});
|
||||
})?;
|
||||
|
||||
let editor = editor.unwrap_or_else(|| {
|
||||
let editor = if let Some(editor) = editor {
|
||||
editor
|
||||
} else {
|
||||
pane.update(&mut cx, |_, cx| {
|
||||
let multibuffer = cx.add_model(|cx| {
|
||||
let mut multibuffer;
|
||||
@@ -115,46 +117,29 @@ impl FollowableItem for Editor {
|
||||
multibuffer
|
||||
});
|
||||
|
||||
cx.add_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), cx))
|
||||
})
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor.remote_id = Some(remote_id);
|
||||
let buffer = editor.buffer.read(cx).read(cx);
|
||||
let selections = state
|
||||
.selections
|
||||
.into_iter()
|
||||
.map(|selection| {
|
||||
deserialize_selection(&buffer, selection)
|
||||
.ok_or_else(|| anyhow!("invalid selection"))
|
||||
cx.add_view(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
|
||||
editor.remote_id = Some(remote_id);
|
||||
editor
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let pending_selection = state
|
||||
.pending_selection
|
||||
.map(|selection| deserialize_selection(&buffer, selection))
|
||||
.flatten();
|
||||
let scroll_top_anchor = state
|
||||
.scroll_top_anchor
|
||||
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
||||
drop(buffer);
|
||||
})?
|
||||
};
|
||||
|
||||
if !selections.is_empty() || pending_selection.is_some() {
|
||||
editor.set_selections_from_remote(selections, pending_selection, cx);
|
||||
}
|
||||
|
||||
if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||
editor.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
top_anchor: scroll_top_anchor,
|
||||
offset: vec2f(state.scroll_x, state.scroll_y),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})?;
|
||||
update_editor_from_message(
|
||||
editor.downgrade(),
|
||||
project,
|
||||
proto::update_view::Editor {
|
||||
selections: state.selections,
|
||||
pending_selection: state.pending_selection,
|
||||
scroll_top_anchor: state.scroll_top_anchor,
|
||||
scroll_x: state.scroll_x,
|
||||
scroll_y: state.scroll_y,
|
||||
..Default::default()
|
||||
},
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(editor)
|
||||
}))
|
||||
@@ -299,96 +284,9 @@ impl FollowableItem for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let update_view::Variant::Editor(message) = message;
|
||||
let multibuffer = self.buffer.read(cx);
|
||||
let multibuffer = multibuffer.read(cx);
|
||||
|
||||
let buffer_ids = message
|
||||
.inserted_excerpts
|
||||
.iter()
|
||||
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut removals = message
|
||||
.deleted_excerpts
|
||||
.into_iter()
|
||||
.map(ExcerptId::from_proto)
|
||||
.collect::<Vec<_>>();
|
||||
removals.sort_by(|a, b| a.cmp(&b, &multibuffer));
|
||||
|
||||
let selections = message
|
||||
.selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| deserialize_selection(&multibuffer, selection))
|
||||
.collect::<Vec<_>>();
|
||||
let pending_selection = message
|
||||
.pending_selection
|
||||
.and_then(|selection| deserialize_selection(&multibuffer, selection));
|
||||
|
||||
let scroll_top_anchor = message
|
||||
.scroll_top_anchor
|
||||
.and_then(|anchor| deserialize_anchor(&multibuffer, anchor));
|
||||
drop(multibuffer);
|
||||
|
||||
let buffers = project.update(cx, |project, cx| {
|
||||
buffer_ids
|
||||
.into_iter()
|
||||
.map(|id| project.open_buffer_by_id(id, cx))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
let project = project.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let _buffers = try_join_all(buffers).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.buffer.update(cx, |multibuffer, cx| {
|
||||
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
||||
while let Some(insertion) = insertions.next() {
|
||||
let Some(excerpt) = insertion.excerpt else { continue };
|
||||
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
|
||||
let buffer_id = excerpt.buffer_id;
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
|
||||
|
||||
let adjacent_excerpts = iter::from_fn(|| {
|
||||
let insertion = insertions.peek()?;
|
||||
if insertion.previous_excerpt_id.is_none()
|
||||
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
||||
{
|
||||
insertions.next()?.excerpt
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
multibuffer.insert_excerpts_with_ids_after(
|
||||
ExcerptId::from_proto(previous_excerpt_id),
|
||||
buffer,
|
||||
[excerpt]
|
||||
.into_iter()
|
||||
.chain(adjacent_excerpts)
|
||||
.filter_map(|excerpt| {
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
deserialize_excerpt_range(excerpt)?,
|
||||
))
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
multibuffer.remove_excerpts(removals, cx);
|
||||
});
|
||||
|
||||
if !selections.is_empty() || pending_selection.is_some() {
|
||||
this.set_selections_from_remote(selections, pending_selection, cx);
|
||||
this.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
} else if let Some(anchor) = scroll_top_anchor {
|
||||
this.set_scroll_anchor_remote(ScrollAnchor {
|
||||
top_anchor: anchor,
|
||||
offset: vec2f(message.scroll_x, message.scroll_y)
|
||||
}, cx);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
update_editor_from_message(this, project, message, &mut cx).await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -402,6 +300,128 @@ impl FollowableItem for Editor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_editor_from_message(
|
||||
this: WeakViewHandle<Editor>,
|
||||
project: ModelHandle<Project>,
|
||||
message: proto::update_view::Editor,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
// Open all of the buffers of which excerpts were added to the editor.
|
||||
let inserted_excerpt_buffer_ids = message
|
||||
.inserted_excerpts
|
||||
.iter()
|
||||
.filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
|
||||
.collect::<HashSet<_>>();
|
||||
let inserted_excerpt_buffers = project.update(cx, |project, cx| {
|
||||
inserted_excerpt_buffer_ids
|
||||
.into_iter()
|
||||
.map(|id| project.open_buffer_by_id(id, cx))
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
|
||||
|
||||
// Update the editor's excerpts.
|
||||
this.update(cx, |editor, cx| {
|
||||
editor.buffer.update(cx, |multibuffer, cx| {
|
||||
let mut removed_excerpt_ids = message
|
||||
.deleted_excerpts
|
||||
.into_iter()
|
||||
.map(ExcerptId::from_proto)
|
||||
.collect::<Vec<_>>();
|
||||
removed_excerpt_ids.sort_by({
|
||||
let multibuffer = multibuffer.read(cx);
|
||||
move |a, b| a.cmp(&b, &multibuffer)
|
||||
});
|
||||
|
||||
let mut insertions = message.inserted_excerpts.into_iter().peekable();
|
||||
while let Some(insertion) = insertions.next() {
|
||||
let Some(excerpt) = insertion.excerpt else { continue };
|
||||
let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue };
|
||||
let buffer_id = excerpt.buffer_id;
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue };
|
||||
|
||||
let adjacent_excerpts = iter::from_fn(|| {
|
||||
let insertion = insertions.peek()?;
|
||||
if insertion.previous_excerpt_id.is_none()
|
||||
&& insertion.excerpt.as_ref()?.buffer_id == buffer_id
|
||||
{
|
||||
insertions.next()?.excerpt
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
multibuffer.insert_excerpts_with_ids_after(
|
||||
ExcerptId::from_proto(previous_excerpt_id),
|
||||
buffer,
|
||||
[excerpt]
|
||||
.into_iter()
|
||||
.chain(adjacent_excerpts)
|
||||
.filter_map(|excerpt| {
|
||||
Some((
|
||||
ExcerptId::from_proto(excerpt.id),
|
||||
deserialize_excerpt_range(excerpt)?,
|
||||
))
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
multibuffer.remove_excerpts(removed_excerpt_ids, cx);
|
||||
});
|
||||
})?;
|
||||
|
||||
// Deserialize the editor state.
|
||||
let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer.read(cx).read(cx);
|
||||
let selections = message
|
||||
.selections
|
||||
.into_iter()
|
||||
.filter_map(|selection| deserialize_selection(&buffer, selection))
|
||||
.collect::<Vec<_>>();
|
||||
let pending_selection = message
|
||||
.pending_selection
|
||||
.and_then(|selection| deserialize_selection(&buffer, selection));
|
||||
let scroll_top_anchor = message
|
||||
.scroll_top_anchor
|
||||
.and_then(|anchor| deserialize_anchor(&buffer, anchor));
|
||||
anyhow::Ok((selections, pending_selection, scroll_top_anchor))
|
||||
})??;
|
||||
|
||||
// Wait until the buffer has received all of the operations referenced by
|
||||
// the editor's new state.
|
||||
this.update(cx, |editor, cx| {
|
||||
editor.buffer.update(cx, |buffer, cx| {
|
||||
buffer.wait_for_anchors(
|
||||
selections
|
||||
.iter()
|
||||
.chain(pending_selection.as_ref())
|
||||
.flat_map(|selection| [selection.start, selection.end])
|
||||
.chain(scroll_top_anchor),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
// Update the editor's state.
|
||||
this.update(cx, |editor, cx| {
|
||||
if !selections.is_empty() || pending_selection.is_some() {
|
||||
editor.set_selections_from_remote(selections, pending_selection, cx);
|
||||
editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
} else if let Some(scroll_top_anchor) = scroll_top_anchor {
|
||||
editor.set_scroll_anchor_remote(
|
||||
ScrollAnchor {
|
||||
top_anchor: scroll_top_anchor,
|
||||
offset: vec2f(message.scroll_x, message.scroll_y),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_excerpt(
|
||||
buffer_id: u64,
|
||||
id: &ExcerptId,
|
||||
@@ -514,25 +534,38 @@ impl Item for Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<'a, str>> {
|
||||
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
|
||||
let file_path = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()?
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|f| f.as_local())?
|
||||
.abs_path(cx);
|
||||
|
||||
let file_path = util::paths::compact(&file_path)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
Some(file_path.into())
|
||||
}
|
||||
|
||||
fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> {
|
||||
match path_for_buffer(&self.buffer, detail, true, cx)? {
|
||||
Cow::Borrowed(path) => Some(path.to_string_lossy()),
|
||||
Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content(
|
||||
fn tab_content<T: View>(
|
||||
&self,
|
||||
detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
cx: &AppContext,
|
||||
) -> ElementBox {
|
||||
) -> AnyElement<T> {
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(self.title(cx).to_string(), style.label.clone())
|
||||
.aligned()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any())
|
||||
.with_children(detail.and_then(|detail| {
|
||||
let path = path_for_buffer(&self.buffer, detail, false, cx)?;
|
||||
let description = path.to_string_lossy();
|
||||
@@ -543,11 +576,11 @@ impl Item for Editor {
|
||||
)
|
||||
.contained()
|
||||
.with_style(style.description.container)
|
||||
.aligned()
|
||||
.boxed(),
|
||||
.aligned(),
|
||||
)
|
||||
}))
|
||||
.boxed()
|
||||
.align_children_center()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
|
||||
@@ -603,10 +636,10 @@ impl Item for Editor {
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.report_event("save editor", cx);
|
||||
self.report_editor_event("save", None, 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 {
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
format.await?;
|
||||
|
||||
if buffers.len() == 1 {
|
||||
@@ -652,6 +685,11 @@ impl Item for Editor {
|
||||
.as_singleton()
|
||||
.expect("cannot call save_as on an excerpt list");
|
||||
|
||||
let file_extension = abs_path
|
||||
.extension()
|
||||
.map(|a| a.to_string_lossy().to_string());
|
||||
self.report_editor_event("save", file_extension, cx);
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.save_buffer_as(buffer, abs_path, cx)
|
||||
})
|
||||
@@ -670,11 +708,11 @@ impl Item for Editor {
|
||||
let transaction = reload_buffers.log_err().await;
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::fit(), cx)
|
||||
});
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
})?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0);
|
||||
buffer.push_transaction(&transaction.0, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -682,17 +720,6 @@ impl Item for Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn git_diff_recalc(
|
||||
&mut self,
|
||||
_project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.buffer().update(cx, |multibuffer, cx| {
|
||||
multibuffer.git_diff_recalc(cx);
|
||||
});
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
|
||||
let mut result = SmallVec::new();
|
||||
match event {
|
||||
@@ -727,7 +754,7 @@ impl Item for Editor {
|
||||
ToolbarItemLocation::PrimaryLeft { flex: None }
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let multibuffer = &self.buffer().read(cx);
|
||||
let (buffer_id, symbols) =
|
||||
@@ -747,15 +774,13 @@ impl Item for Editor {
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "untitled".to_string());
|
||||
|
||||
let filename_label = Label::new(filename, theme.workspace.breadcrumbs.default.text.clone());
|
||||
let mut breadcrumbs = vec![filename_label.boxed()];
|
||||
breadcrumbs.extend(symbols.into_iter().map(|symbol| {
|
||||
Text::new(
|
||||
symbol.text,
|
||||
theme.workspace.breadcrumbs.default.text.clone(),
|
||||
)
|
||||
.with_highlights(symbol.highlight_ranges)
|
||||
.boxed()
|
||||
let mut breadcrumbs = vec![BreadcrumbText {
|
||||
text: filename,
|
||||
highlights: None,
|
||||
}];
|
||||
breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
|
||||
text: symbol.text,
|
||||
highlights: Some(symbol.highlight_ranges),
|
||||
}));
|
||||
Some(breadcrumbs)
|
||||
}
|
||||
@@ -763,7 +788,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);
|
||||
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
|
||||
|
||||
fn serialize(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
@@ -788,9 +813,9 @@ impl Item for Editor {
|
||||
serialize(buffer.clone(), workspace_id, item_id, cx);
|
||||
|
||||
cx.subscribe(&buffer, |this, buffer, event, cx| {
|
||||
if let Some(workspace_id) = this.workspace_id {
|
||||
if let Some((_, workspace_id)) = this.workspace.as_ref() {
|
||||
if let language::Event::FileHandleChanged = event {
|
||||
serialize(buffer, workspace_id, cx.view_id(), cx);
|
||||
serialize(buffer, *workspace_id, cx.view_id(), cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -833,14 +858,13 @@ impl Item for Editor {
|
||||
let buffer = project_item
|
||||
.downcast::<Buffer>()
|
||||
.context("Project item at stored path was not a buffer")?;
|
||||
|
||||
Ok(cx.update(|cx| {
|
||||
cx.add_view(&pane, |cx| {
|
||||
Ok(pane.update(&mut cx, |_, cx| {
|
||||
cx.add_view(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||
editor
|
||||
})
|
||||
}))
|
||||
})?)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||
@@ -1078,16 +1102,20 @@ impl View for CursorPosition {
|
||||
"CursorPosition"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
if let Some(position) = self.position {
|
||||
let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
let mut text = format!("{},{}", position.row + 1, position.column + 1);
|
||||
let theme = &theme::current(cx).workspace.status_bar;
|
||||
let mut text = format!(
|
||||
"{}{FILE_ROW_COLUMN_DELIMITER}{}",
|
||||
position.row + 1,
|
||||
position.column + 1
|
||||
);
|
||||
if self.selected_count > 0 {
|
||||
write!(text, " ({} selected)", self.selected_count).unwrap();
|
||||
}
|
||||
Label::new(text, theme.cursor_position.clone()).boxed()
|
||||
Label::new(text, theme.cursor_position.clone()).into_any()
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
Empty::new().into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1192,27 +1220,27 @@ mod tests {
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn mtime(&self) -> SystemTime {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
todo!()
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,9 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::{impl_internal_actions, AppContext, Task, ViewContext};
|
||||
use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase};
|
||||
use gpui::{Task, ViewContext};
|
||||
use language::{Bias, ToOffset};
|
||||
use project::LocationLink;
|
||||
use settings::Settings;
|
||||
use std::ops::Range;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{
|
||||
Anchor, DisplayPoint, Editor, EditorSnapshot, GoToDefinition, GoToTypeDefinition, Select,
|
||||
SelectPhase,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct UpdateGoToDefinitionLink {
|
||||
pub point: Option<DisplayPoint>,
|
||||
pub cmd_held: bool,
|
||||
pub shift_held: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct GoToFetchedDefinition {
|
||||
pub point: DisplayPoint,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct GoToFetchedTypeDefinition {
|
||||
pub point: DisplayPoint,
|
||||
}
|
||||
|
||||
impl_internal_actions!(
|
||||
editor,
|
||||
[
|
||||
UpdateGoToDefinitionLink,
|
||||
GoToFetchedDefinition,
|
||||
GoToFetchedTypeDefinition
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(update_go_to_definition_link);
|
||||
cx.add_action(go_to_fetched_definition);
|
||||
cx.add_action(go_to_fetched_type_definition);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LinkGoToDefinitionState {
|
||||
@@ -55,11 +16,9 @@ pub struct LinkGoToDefinitionState {
|
||||
|
||||
pub fn update_go_to_definition_link(
|
||||
editor: &mut Editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point,
|
||||
cmd_held,
|
||||
shift_held,
|
||||
}: &UpdateGoToDefinitionLink,
|
||||
point: Option<DisplayPoint>,
|
||||
cmd_held: bool,
|
||||
shift_held: bool,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let pending_nonempty_selection = editor.has_pending_nonempty_selection();
|
||||
@@ -171,7 +130,7 @@ pub fn show_link_definition(
|
||||
}
|
||||
}
|
||||
|
||||
let task = cx.spawn_weak(|this, mut cx| {
|
||||
let task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
// query the LSP for definition info
|
||||
let definition_request = cx.update(|cx| {
|
||||
@@ -202,67 +161,65 @@ pub fn show_link_definition(
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(this) = this.upgrade(&cx) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// Clear any existing highlights
|
||||
this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
|
||||
this.link_go_to_definition_state.kind = Some(definition_kind);
|
||||
this.link_go_to_definition_state.symbol_range = result
|
||||
.as_ref()
|
||||
.and_then(|(symbol_range, _)| symbol_range.clone());
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// Clear any existing highlights
|
||||
this.clear_text_highlights::<LinkGoToDefinitionState>(cx);
|
||||
this.link_go_to_definition_state.kind = Some(definition_kind);
|
||||
this.link_go_to_definition_state.symbol_range = result
|
||||
.as_ref()
|
||||
.and_then(|(symbol_range, _)| symbol_range.clone());
|
||||
|
||||
if let Some((symbol_range, definitions)) = result {
|
||||
this.link_go_to_definition_state.definitions = definitions.clone();
|
||||
if let Some((symbol_range, definitions)) = result {
|
||||
this.link_go_to_definition_state.definitions = definitions.clone();
|
||||
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
// Only show highlight if there exists a definition to jump to that doesn't contain
|
||||
// the current location.
|
||||
let any_definition_does_not_contain_current_location =
|
||||
definitions.iter().any(|definition| {
|
||||
let target = &definition.target;
|
||||
if target.buffer == buffer {
|
||||
let range = &target.range;
|
||||
// Expand range by one character as lsp definition ranges include positions adjacent
|
||||
// but not contained by the symbol range
|
||||
let start = buffer_snapshot.clip_offset(
|
||||
range.start.to_offset(&buffer_snapshot).saturating_sub(1),
|
||||
Bias::Left,
|
||||
);
|
||||
let end = buffer_snapshot.clip_offset(
|
||||
range.end.to_offset(&buffer_snapshot) + 1,
|
||||
Bias::Right,
|
||||
);
|
||||
let offset = buffer_position.to_offset(&buffer_snapshot);
|
||||
!(start <= offset && end >= offset)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
// Only show highlight if there exists a definition to jump to that doesn't contain
|
||||
// the current location.
|
||||
let any_definition_does_not_contain_current_location =
|
||||
definitions.iter().any(|definition| {
|
||||
let target = &definition.target;
|
||||
if target.buffer == buffer {
|
||||
let range = &target.range;
|
||||
// Expand range by one character as lsp definition ranges include positions adjacent
|
||||
// but not contained by the symbol range
|
||||
let start = buffer_snapshot.clip_offset(
|
||||
range.start.to_offset(&buffer_snapshot).saturating_sub(1),
|
||||
Bias::Left,
|
||||
);
|
||||
let end = buffer_snapshot.clip_offset(
|
||||
range.end.to_offset(&buffer_snapshot) + 1,
|
||||
Bias::Right,
|
||||
);
|
||||
let offset = buffer_position.to_offset(&buffer_snapshot);
|
||||
!(start <= offset && end >= offset)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if any_definition_does_not_contain_current_location {
|
||||
// If no symbol range returned from language server, use the surrounding word.
|
||||
let highlight_range = symbol_range.unwrap_or_else(|| {
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let (offset_range, _) = snapshot.surrounding_word(trigger_point);
|
||||
if any_definition_does_not_contain_current_location {
|
||||
// If no symbol range returned from language server, use the surrounding word.
|
||||
let highlight_range = symbol_range.unwrap_or_else(|| {
|
||||
let snapshot = &snapshot.buffer_snapshot;
|
||||
let (offset_range, _) = snapshot.surrounding_word(trigger_point);
|
||||
|
||||
snapshot.anchor_before(offset_range.start)
|
||||
..snapshot.anchor_after(offset_range.end)
|
||||
});
|
||||
snapshot.anchor_before(offset_range.start)
|
||||
..snapshot.anchor_after(offset_range.end)
|
||||
});
|
||||
|
||||
// Highlight symbol using theme link definition highlight style
|
||||
let style = cx.global::<Settings>().theme.editor.link_definition;
|
||||
this.highlight_text::<LinkGoToDefinitionState>(
|
||||
vec![highlight_range],
|
||||
style,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
hide_link_definition(this, cx);
|
||||
}
|
||||
// Highlight symbol using theme link definition highlight style
|
||||
let style = theme::current(cx).editor.link_definition;
|
||||
this.highlight_text::<LinkGoToDefinitionState>(
|
||||
vec![highlight_range],
|
||||
style,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
hide_link_definition(this, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
}
|
||||
@@ -287,76 +244,59 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
|
||||
}
|
||||
|
||||
pub fn go_to_fetched_definition(
|
||||
workspace: &mut Workspace,
|
||||
&GoToFetchedDefinition { point }: &GoToFetchedDefinition,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, workspace, point, cx);
|
||||
go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, cx);
|
||||
}
|
||||
|
||||
pub fn go_to_fetched_type_definition(
|
||||
workspace: &mut Workspace,
|
||||
&GoToFetchedTypeDefinition { point }: &GoToFetchedTypeDefinition,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, workspace, point, cx);
|
||||
go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, cx);
|
||||
}
|
||||
|
||||
fn go_to_fetched_definition_of_kind(
|
||||
kind: LinkDefinitionKind,
|
||||
workspace: &mut Workspace,
|
||||
editor: &mut Editor,
|
||||
point: DisplayPoint,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
let active_item = workspace.active_item(cx);
|
||||
let editor_handle = if let Some(editor) = active_item
|
||||
.as_ref()
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
{
|
||||
editor
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (cached_definitions, cached_definitions_kind) = editor_handle.update(cx, |editor, cx| {
|
||||
let definitions = editor.link_go_to_definition_state.definitions.clone();
|
||||
hide_link_definition(editor, cx);
|
||||
(definitions, editor.link_go_to_definition_state.kind)
|
||||
});
|
||||
let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
|
||||
hide_link_definition(editor, cx);
|
||||
let cached_definitions_kind = editor.link_go_to_definition_state.kind;
|
||||
|
||||
let is_correct_kind = cached_definitions_kind == Some(kind);
|
||||
if !cached_definitions.is_empty() && is_correct_kind {
|
||||
editor_handle.update(cx, |editor, cx| {
|
||||
if !editor.focused {
|
||||
cx.focus_self();
|
||||
}
|
||||
});
|
||||
if !editor.focused {
|
||||
cx.focus_self();
|
||||
}
|
||||
|
||||
Editor::navigate_to_definitions(workspace, editor_handle, cached_definitions, cx);
|
||||
editor.navigate_to_definitions(cached_definitions, cx);
|
||||
} else {
|
||||
editor_handle.update(cx, |editor, cx| {
|
||||
editor.select(
|
||||
&Select(SelectPhase::Begin {
|
||||
position: point,
|
||||
add: false,
|
||||
click_count: 1,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
editor.select(
|
||||
SelectPhase::Begin {
|
||||
position: point,
|
||||
add: false,
|
||||
click_count: 1,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
match kind {
|
||||
LinkDefinitionKind::Symbol => Editor::go_to_definition(workspace, &GoToDefinition, cx),
|
||||
|
||||
LinkDefinitionKind::Type => {
|
||||
Editor::go_to_type_definition(workspace, &GoToTypeDefinition, cx)
|
||||
}
|
||||
LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
|
||||
LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
platform::{self, Modifiers, ModifiersChangedEvent},
|
||||
@@ -365,12 +305,10 @@ mod tests {
|
||||
use indoc::indoc;
|
||||
use lsp::request::{GotoDefinition, GotoTypeDefinition};
|
||||
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
@@ -413,15 +351,7 @@ mod tests {
|
||||
|
||||
// Press cmd+shift to trigger highlight
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point: Some(hover_point),
|
||||
cmd_held: true,
|
||||
shift_held: true,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
update_go_to_definition_link(editor, Some(hover_point), true, true, cx);
|
||||
});
|
||||
requests.next().await;
|
||||
cx.foreground().run_until_parked();
|
||||
@@ -471,12 +401,8 @@ mod tests {
|
||||
])))
|
||||
});
|
||||
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
go_to_fetched_type_definition(
|
||||
workspace,
|
||||
&GoToFetchedTypeDefinition { point: hover_point },
|
||||
cx,
|
||||
);
|
||||
cx.update_editor(|editor, cx| {
|
||||
go_to_fetched_type_definition(editor, hover_point, cx);
|
||||
});
|
||||
requests.next().await;
|
||||
cx.foreground().run_until_parked();
|
||||
@@ -489,6 +415,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
@@ -529,15 +457,7 @@ mod tests {
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point: Some(hover_point),
|
||||
cmd_held: true,
|
||||
shift_held: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
|
||||
});
|
||||
requests.next().await;
|
||||
cx.foreground().run_until_parked();
|
||||
@@ -571,15 +491,7 @@ mod tests {
|
||||
])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point: Some(hover_point),
|
||||
cmd_held: true,
|
||||
shift_held: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
|
||||
});
|
||||
requests.next().await;
|
||||
cx.foreground().run_until_parked();
|
||||
@@ -601,15 +513,7 @@ mod tests {
|
||||
Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point: Some(hover_point),
|
||||
cmd_held: true,
|
||||
shift_held: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
|
||||
});
|
||||
requests.next().await;
|
||||
cx.foreground().run_until_parked();
|
||||
@@ -626,15 +530,7 @@ mod tests {
|
||||
fn do_work() { teˇst(); }
|
||||
"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point: Some(hover_point),
|
||||
cmd_held: false,
|
||||
shift_held: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
update_go_to_definition_link(editor, Some(hover_point), false, false, cx);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
|
||||
@@ -693,15 +589,7 @@ mod tests {
|
||||
|
||||
// Moving the mouse restores the highlights.
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point: Some(hover_point),
|
||||
cmd_held: true,
|
||||
shift_held: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
|
||||
@@ -715,15 +603,7 @@ mod tests {
|
||||
fn do_work() { tesˇt(); }
|
||||
"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point: Some(hover_point),
|
||||
cmd_held: true,
|
||||
shift_held: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
|
||||
@@ -732,8 +612,8 @@ mod tests {
|
||||
"});
|
||||
|
||||
// Cmd click with existing definition doesn't re-request and dismisses highlight
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
|
||||
cx.update_editor(|editor, cx| {
|
||||
go_to_fetched_definition(editor, hover_point, cx);
|
||||
});
|
||||
// Assert selection moved to to definition
|
||||
cx.lsp
|
||||
@@ -773,8 +653,8 @@ mod tests {
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.update_workspace(|workspace, cx| {
|
||||
go_to_fetched_definition(workspace, &GoToFetchedDefinition { point: hover_point }, cx);
|
||||
cx.update_editor(|editor, cx| {
|
||||
go_to_fetched_definition(editor, hover_point, cx);
|
||||
});
|
||||
requests.next().await;
|
||||
cx.foreground().run_until_parked();
|
||||
@@ -819,15 +699,7 @@ mod tests {
|
||||
});
|
||||
});
|
||||
cx.update_editor(|editor, cx| {
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
&UpdateGoToDefinitionLink {
|
||||
point: Some(hover_point),
|
||||
cmd_held: true,
|
||||
shift_held: false,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
update_go_to_definition_link(editor, Some(hover_point), true, false, cx);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert!(requests.try_next().is_err());
|
||||
|
||||
@@ -1,29 +1,14 @@
|
||||
use context_menu::ContextMenuItem;
|
||||
use gpui::{
|
||||
elements::AnchorCorner, geometry::vector::Vector2F, impl_internal_actions, AppContext,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
|
||||
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
||||
};
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployMouseContextMenu {
|
||||
pub position: Vector2F,
|
||||
pub point: DisplayPoint,
|
||||
}
|
||||
|
||||
impl_internal_actions!(editor, [DeployMouseContextMenu]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(deploy_context_menu);
|
||||
}
|
||||
use context_menu::ContextMenuItem;
|
||||
use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext};
|
||||
|
||||
pub fn deploy_context_menu(
|
||||
editor: &mut Editor,
|
||||
&DeployMouseContextMenu { position, point }: &DeployMouseContextMenu,
|
||||
position: Vector2F,
|
||||
point: DisplayPoint,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) {
|
||||
if !editor.focused {
|
||||
@@ -51,18 +36,18 @@ pub fn deploy_context_menu(
|
||||
position,
|
||||
AnchorCorner::TopLeft,
|
||||
vec![
|
||||
ContextMenuItem::item("Rename Symbol", Rename),
|
||||
ContextMenuItem::item("Go to Definition", GoToDefinition),
|
||||
ContextMenuItem::item("Go to Type Definition", GoToTypeDefinition),
|
||||
ContextMenuItem::item("Find All References", FindAllReferences),
|
||||
ContextMenuItem::item(
|
||||
ContextMenuItem::action("Rename Symbol", Rename),
|
||||
ContextMenuItem::action("Go to Definition", GoToDefinition),
|
||||
ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
|
||||
ContextMenuItem::action("Find All References", FindAllReferences),
|
||||
ContextMenuItem::action(
|
||||
"Code Actions",
|
||||
ToggleCodeActions {
|
||||
deployed_from_indicator: false,
|
||||
},
|
||||
),
|
||||
ContextMenuItem::Separator,
|
||||
ContextMenuItem::item("Reveal in Finder", RevealInFinder),
|
||||
ContextMenuItem::action("Reveal in Finder", RevealInFinder),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -72,13 +57,14 @@ pub fn deploy_context_menu(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
|
||||
use super::*;
|
||||
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
||||
@@ -98,16 +84,7 @@ mod tests {
|
||||
do_wˇork();
|
||||
}
|
||||
"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
deploy_context_menu(
|
||||
editor,
|
||||
&DeployMouseContextMenu {
|
||||
position: Default::default(),
|
||||
point,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn test() {
|
||||
|
||||
@@ -193,6 +193,44 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == 0 {
|
||||
return map.max_point();
|
||||
}
|
||||
|
||||
let mut found_non_blank_line = false;
|
||||
for row in (0..point.row + 1).rev() {
|
||||
let blank = map.buffer_snapshot.is_line_blank(row);
|
||||
if found_non_blank_line && blank {
|
||||
return Point::new(row, 0).to_display_point(map);
|
||||
}
|
||||
|
||||
found_non_blank_line |= !blank;
|
||||
}
|
||||
|
||||
DisplayPoint::zero()
|
||||
}
|
||||
|
||||
pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == map.max_buffer_row() {
|
||||
return DisplayPoint::zero();
|
||||
}
|
||||
|
||||
let mut found_non_blank_line = false;
|
||||
for row in point.row..map.max_buffer_row() + 1 {
|
||||
let blank = map.buffer_snapshot.is_line_blank(row);
|
||||
if found_non_blank_line && blank {
|
||||
return Point::new(row, 0).to_display_point(map);
|
||||
}
|
||||
|
||||
found_non_blank_line |= !blank;
|
||||
}
|
||||
|
||||
map.max_point()
|
||||
}
|
||||
|
||||
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
|
||||
/// given predicate returning true. The predicate is called with the character to the left and right
|
||||
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
|
||||
@@ -369,11 +407,12 @@ pub fn split_display_range_by_lines(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_previous_word_start(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
@@ -400,7 +439,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_previous_subword_start(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
@@ -434,7 +474,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
fn assert(
|
||||
marked_text: &str,
|
||||
cx: &mut gpui::AppContext,
|
||||
@@ -466,7 +507,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_next_word_end(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
@@ -490,7 +532,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_next_subword_end(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
@@ -523,7 +566,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_find_boundary(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
fn assert(
|
||||
marked_text: &str,
|
||||
cx: &mut gpui::AppContext,
|
||||
@@ -555,7 +599,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_surrounding_word(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
|
||||
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
|
||||
assert_eq!(
|
||||
@@ -576,7 +621,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_move_up_and_down_with_excerpts(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
init_test(cx);
|
||||
|
||||
let family_id = cx
|
||||
.font_cache()
|
||||
.load_family(&["Helvetica"], &Default::default())
|
||||
@@ -691,4 +737,11 @@ mod tests {
|
||||
(DisplayPoint::new(7, 2), SelectionGoal::Column(2)),
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::AppContext) {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
language::init(cx);
|
||||
crate::init(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod anchor;
|
||||
|
||||
pub use anchor::{Anchor, AnchorRangeExt};
|
||||
use anyhow::{anyhow, Result};
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, SinkExt};
|
||||
@@ -8,15 +9,19 @@ use git::diff::DiffHunk;
|
||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
pub use language::Completion;
|
||||
use language::{
|
||||
char_kind, AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
|
||||
DiagnosticEntry, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline,
|
||||
OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _, ToOffsetUtf16 as _,
|
||||
ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||
char_kind,
|
||||
language_settings::{language_settings, LanguageSettings},
|
||||
AutoindentMode, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, CursorShape,
|
||||
DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
|
||||
Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
|
||||
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::{Ref, RefCell},
|
||||
cmp, fmt, io,
|
||||
cmp, fmt,
|
||||
future::Future,
|
||||
io,
|
||||
iter::{self, FromIterator},
|
||||
mem,
|
||||
ops::{Range, RangeBounds, Sub},
|
||||
@@ -40,7 +45,7 @@ pub struct ExcerptId(usize);
|
||||
|
||||
pub struct MultiBuffer {
|
||||
snapshot: RefCell<MultiBufferSnapshot>,
|
||||
buffers: RefCell<HashMap<usize, BufferState>>,
|
||||
buffers: RefCell<HashMap<u64, BufferState>>,
|
||||
next_excerpt_id: usize,
|
||||
subscriptions: Topic,
|
||||
singleton: bool,
|
||||
@@ -61,6 +66,8 @@ pub enum Event {
|
||||
},
|
||||
Edited,
|
||||
Reloaded,
|
||||
DiffBaseChanged,
|
||||
LanguageChanged,
|
||||
Reparsed,
|
||||
Saved,
|
||||
FileHandleChanged,
|
||||
@@ -81,7 +88,7 @@ struct History {
|
||||
#[derive(Clone)]
|
||||
struct Transaction {
|
||||
id: TransactionId,
|
||||
buffer_transactions: HashMap<usize, text::TransactionId>,
|
||||
buffer_transactions: HashMap<u64, text::TransactionId>,
|
||||
first_edit_at: Instant,
|
||||
last_edit_at: Instant,
|
||||
suppress_grouping: bool,
|
||||
@@ -141,7 +148,7 @@ pub struct ExcerptBoundary {
|
||||
struct Excerpt {
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
buffer_id: usize,
|
||||
buffer_id: u64,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
max_buffer_row: u32,
|
||||
@@ -333,21 +340,10 @@ impl MultiBuffer {
|
||||
offset: T,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
cx: &AppContext,
|
||||
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
|
||||
) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
|
||||
self.read(cx).symbols_containing(offset, theme)
|
||||
}
|
||||
|
||||
pub fn git_diff_recalc(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let buffers = self.buffers.borrow();
|
||||
for buffer_state in buffers.values() {
|
||||
if buffer_state.buffer.read(cx).needs_git_diff_recalc() {
|
||||
buffer_state
|
||||
.buffer
|
||||
.update(cx, |buffer, cx| buffer.git_diff_recalc(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit<I, S, T>(
|
||||
&mut self,
|
||||
edits: I,
|
||||
@@ -390,7 +386,7 @@ impl MultiBuffer {
|
||||
is_insertion: bool,
|
||||
original_indent_column: u32,
|
||||
}
|
||||
let mut buffer_edits: HashMap<usize, Vec<BufferEdit>> = Default::default();
|
||||
let mut buffer_edits: HashMap<u64, Vec<BufferEdit>> = Default::default();
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
for (ix, (range, new_text)) in edits.enumerate() {
|
||||
let new_text: Arc<str> = new_text.into();
|
||||
@@ -589,7 +585,7 @@ impl MultiBuffer {
|
||||
if let Some(transaction_id) =
|
||||
buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
|
||||
{
|
||||
buffer_transactions.insert(buffer.id(), transaction_id);
|
||||
buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,12 +606,12 @@ impl MultiBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T)
|
||||
pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &mut ModelContext<Self>)
|
||||
where
|
||||
T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
|
||||
{
|
||||
self.history
|
||||
.push_transaction(buffer_transactions, Instant::now());
|
||||
.push_transaction(buffer_transactions, Instant::now(), cx);
|
||||
self.history.finalize_last_transaction();
|
||||
}
|
||||
|
||||
@@ -640,7 +636,7 @@ impl MultiBuffer {
|
||||
cursor_shape: CursorShape,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let mut selections_by_buffer: HashMap<usize, Vec<Selection<text::Anchor>>> =
|
||||
let mut selections_by_buffer: HashMap<u64, Vec<Selection<text::Anchor>>> =
|
||||
Default::default();
|
||||
let snapshot = self.read(cx);
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
@@ -781,8 +777,8 @@ impl MultiBuffer {
|
||||
let (mut tx, rx) = mpsc::channel(256);
|
||||
let task = cx.spawn(|this, mut cx| async move {
|
||||
for (buffer, ranges) in excerpts {
|
||||
let buffer_id = buffer.id();
|
||||
let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot());
|
||||
let (buffer_id, buffer_snapshot) =
|
||||
buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot()));
|
||||
|
||||
let mut excerpt_ranges = Vec::new();
|
||||
let mut range_counts = Vec::new();
|
||||
@@ -851,7 +847,7 @@ impl MultiBuffer {
|
||||
where
|
||||
O: text::ToPoint + text::ToOffset,
|
||||
{
|
||||
let buffer_id = buffer.id();
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let (excerpt_ranges, range_counts) =
|
||||
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
|
||||
@@ -920,7 +916,7 @@ impl MultiBuffer {
|
||||
|
||||
self.sync(cx);
|
||||
|
||||
let buffer_id = buffer.id();
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
@@ -1047,7 +1043,7 @@ impl MultiBuffer {
|
||||
let buffers = self.buffers.borrow();
|
||||
let mut cursor = snapshot.excerpts.cursor::<Option<&Locator>>();
|
||||
for locator in buffers
|
||||
.get(&buffer.id())
|
||||
.get(&buffer.read(cx).remote_id())
|
||||
.map(|state| &state.excerpts)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -1134,6 +1130,10 @@ impl MultiBuffer {
|
||||
let mut result = Vec::new();
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
cursor.seek(&start, Bias::Right, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.prev(&());
|
||||
}
|
||||
|
||||
while let Some(excerpt) = cursor.item() {
|
||||
if *cursor.start() > end {
|
||||
break;
|
||||
@@ -1161,6 +1161,9 @@ impl MultiBuffer {
|
||||
) {
|
||||
self.sync(cx);
|
||||
let ids = excerpt_ids.into_iter().collect::<Vec<_>>();
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut buffers = self.buffers.borrow_mut();
|
||||
let mut snapshot = self.snapshot.borrow_mut();
|
||||
@@ -1238,6 +1241,39 @@ impl MultiBuffer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn wait_for_anchors<'a>(
|
||||
&self,
|
||||
anchors: impl 'a + Iterator<Item = Anchor>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> impl 'static + Future<Output = Result<()>> {
|
||||
let borrow = self.buffers.borrow();
|
||||
let mut error = None;
|
||||
let mut futures = Vec::new();
|
||||
for anchor in anchors {
|
||||
if let Some(buffer_id) = anchor.buffer_id {
|
||||
if let Some(buffer) = borrow.get(&buffer_id) {
|
||||
buffer.buffer.update(cx, |buffer, _| {
|
||||
futures.push(buffer.wait_for_anchors([anchor.text_anchor]))
|
||||
});
|
||||
} else {
|
||||
error = Some(anyhow!(
|
||||
"buffer {buffer_id} is not part of this multi-buffer"
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
async move {
|
||||
if let Some(error) = error {
|
||||
Err(error)?;
|
||||
}
|
||||
for future in futures {
|
||||
future.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_anchor_for_position<T: ToOffset>(
|
||||
&self,
|
||||
position: T,
|
||||
@@ -1266,6 +1302,8 @@ impl MultiBuffer {
|
||||
language::Event::Saved => Event::Saved,
|
||||
language::Event::FileHandleChanged => Event::FileHandleChanged,
|
||||
language::Event::Reloaded => Event::Reloaded,
|
||||
language::Event::DiffBaseChanged => Event::DiffBaseChanged,
|
||||
language::Event::LanguageChanged => Event::LanguageChanged,
|
||||
language::Event::Reparsed => Event::Reparsed,
|
||||
language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
||||
language::Event::Closed => Event::Closed,
|
||||
@@ -1283,7 +1321,7 @@ impl MultiBuffer {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn buffer(&self, buffer_id: usize) -> Option<ModelHandle<Buffer>> {
|
||||
pub fn buffer(&self, buffer_id: u64) -> Option<ModelHandle<Buffer>> {
|
||||
self.buffers
|
||||
.borrow()
|
||||
.get(&buffer_id)
|
||||
@@ -1334,6 +1372,15 @@ impl MultiBuffer {
|
||||
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
|
||||
}
|
||||
|
||||
pub fn settings_at<'a, T: ToOffset>(
|
||||
&self,
|
||||
point: T,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
let language = self.language_at(point, cx);
|
||||
language_settings(language.map(|l| l.name()).as_deref(), cx)
|
||||
}
|
||||
|
||||
pub fn for_each_buffer(&self, mut f: impl FnMut(&ModelHandle<Buffer>)) {
|
||||
self.buffers
|
||||
.borrow()
|
||||
@@ -1440,8 +1487,8 @@ impl MultiBuffer {
|
||||
for (locator, buffer, buffer_edited) in excerpts_to_edit {
|
||||
new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &());
|
||||
let old_excerpt = cursor.item().unwrap();
|
||||
let buffer_id = buffer.id();
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_id = buffer.remote_id();
|
||||
|
||||
let mut new_excerpt;
|
||||
if buffer_edited {
|
||||
@@ -1494,6 +1541,13 @@ impl MultiBuffer {
|
||||
cx.add_model(|cx| Self::singleton(buffer, cx))
|
||||
}
|
||||
|
||||
pub fn build_from_buffer(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
cx: &mut gpui::AppContext,
|
||||
) -> ModelHandle<Self> {
|
||||
cx.add_model(|cx| Self::singleton(buffer, cx))
|
||||
}
|
||||
|
||||
pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::AppContext) -> ModelHandle<Self> {
|
||||
cx.add_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0);
|
||||
@@ -1567,11 +1621,11 @@ impl MultiBuffer {
|
||||
let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
|
||||
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
|
||||
buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx)));
|
||||
let buffer = buffers.last().unwrap();
|
||||
let buffer = buffers.last().unwrap().read(cx);
|
||||
log::info!(
|
||||
"Creating new buffer {} with text: {:?}",
|
||||
buffer.id(),
|
||||
buffer.read(cx).text()
|
||||
buffer.remote_id(),
|
||||
buffer.text()
|
||||
);
|
||||
buffers.last().unwrap().clone()
|
||||
} else {
|
||||
@@ -1599,7 +1653,7 @@ impl MultiBuffer {
|
||||
.collect::<Vec<_>>();
|
||||
log::info!(
|
||||
"Inserting excerpts from buffer {} and ranges {:?}: {:?}",
|
||||
buffer_handle.id(),
|
||||
buffer_handle.read(cx).remote_id(),
|
||||
ranges.iter().map(|r| &r.context).collect::<Vec<_>>(),
|
||||
ranges
|
||||
.iter()
|
||||
@@ -1792,7 +1846,7 @@ impl MultiBufferSnapshot {
|
||||
(start..end, word_kind)
|
||||
}
|
||||
|
||||
pub fn as_singleton(&self) -> Option<(&ExcerptId, usize, &BufferSnapshot)> {
|
||||
pub fn as_singleton(&self) -> Option<(&ExcerptId, u64, &BufferSnapshot)> {
|
||||
if self.singleton {
|
||||
self.excerpts
|
||||
.iter()
|
||||
@@ -2716,16 +2770,40 @@ impl MultiBufferSnapshot {
|
||||
self.trailing_excerpt_update_count
|
||||
}
|
||||
|
||||
pub fn file_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<dyn File>> {
|
||||
self.point_to_buffer_offset(point)
|
||||
.and_then(|(buffer, _)| buffer.file())
|
||||
}
|
||||
|
||||
pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
|
||||
self.point_to_buffer_offset(point)
|
||||
.and_then(|(buffer, offset)| buffer.language_at(offset))
|
||||
}
|
||||
|
||||
pub fn settings_at<'a, T: ToOffset>(
|
||||
&'a self,
|
||||
point: T,
|
||||
cx: &'a AppContext,
|
||||
) -> &'a LanguageSettings {
|
||||
self.point_to_buffer_offset(point)
|
||||
.map(|(buffer, offset)| buffer.settings_at(offset, cx))
|
||||
.unwrap_or_else(|| language_settings(None, cx))
|
||||
}
|
||||
|
||||
pub fn language_scope_at<'a, T: ToOffset>(&'a self, point: T) -> Option<LanguageScope> {
|
||||
self.point_to_buffer_offset(point)
|
||||
.and_then(|(buffer, offset)| buffer.language_scope_at(offset))
|
||||
}
|
||||
|
||||
pub fn language_indent_size_at<T: ToOffset>(
|
||||
&self,
|
||||
position: T,
|
||||
cx: &AppContext,
|
||||
) -> Option<IndentSize> {
|
||||
let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?;
|
||||
Some(buffer_snapshot.language_indent_size_at(offset, cx))
|
||||
}
|
||||
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
self.is_dirty
|
||||
}
|
||||
@@ -2753,7 +2831,7 @@ impl MultiBufferSnapshot {
|
||||
) -> impl Iterator<Item = DiagnosticEntry<O>> + 'a
|
||||
where
|
||||
T: 'a + ToOffset,
|
||||
O: 'a + text::FromAnchor,
|
||||
O: 'a + text::FromAnchor + Ord,
|
||||
{
|
||||
self.as_singleton()
|
||||
.into_iter()
|
||||
@@ -2765,20 +2843,24 @@ impl MultiBufferSnapshot {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range<'a>(
|
||||
pub fn has_git_diffs(&self) -> bool {
|
||||
for excerpt in self.excerpts.iter() {
|
||||
if !excerpt.buffer.git_diff.is_empty() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range_rev<'a>(
|
||||
&'a self,
|
||||
row_range: Range<u32>,
|
||||
reversed: bool,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let mut cursor = self.excerpts.cursor::<Point>();
|
||||
|
||||
if reversed {
|
||||
cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.prev(&());
|
||||
}
|
||||
} else {
|
||||
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
|
||||
cursor.seek(&Point::new(row_range.end, 0), Bias::Left, &());
|
||||
if cursor.item().is_none() {
|
||||
cursor.prev(&());
|
||||
}
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
@@ -2808,7 +2890,7 @@ impl MultiBufferSnapshot {
|
||||
|
||||
let buffer_hunks = excerpt
|
||||
.buffer
|
||||
.git_diff_hunks_intersecting_range(buffer_start..buffer_end, reversed)
|
||||
.git_diff_hunks_intersecting_range_rev(buffer_start..buffer_end)
|
||||
.filter_map(move |hunk| {
|
||||
let start = multibuffer_start.row
|
||||
+ hunk
|
||||
@@ -2828,12 +2910,70 @@ impl MultiBufferSnapshot {
|
||||
})
|
||||
});
|
||||
|
||||
if reversed {
|
||||
cursor.prev(&());
|
||||
} else {
|
||||
cursor.next(&());
|
||||
cursor.prev(&());
|
||||
|
||||
Some(buffer_hunks)
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub fn git_diff_hunks_in_range<'a>(
|
||||
&'a self,
|
||||
row_range: Range<u32>,
|
||||
) -> impl 'a + Iterator<Item = DiffHunk<u32>> {
|
||||
let mut cursor = self.excerpts.cursor::<Point>();
|
||||
|
||||
cursor.seek(&Point::new(row_range.start, 0), Bias::Right, &());
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
let excerpt = cursor.item()?;
|
||||
let multibuffer_start = *cursor.start();
|
||||
let multibuffer_end = multibuffer_start + excerpt.text_summary.lines;
|
||||
if multibuffer_start.row >= row_range.end {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut buffer_start = excerpt.range.context.start;
|
||||
let mut buffer_end = excerpt.range.context.end;
|
||||
let excerpt_start_point = buffer_start.to_point(&excerpt.buffer);
|
||||
let excerpt_end_point = excerpt_start_point + excerpt.text_summary.lines;
|
||||
|
||||
if row_range.start > multibuffer_start.row {
|
||||
let buffer_start_point =
|
||||
excerpt_start_point + Point::new(row_range.start - multibuffer_start.row, 0);
|
||||
buffer_start = excerpt.buffer.anchor_before(buffer_start_point);
|
||||
}
|
||||
|
||||
if row_range.end < multibuffer_end.row {
|
||||
let buffer_end_point =
|
||||
excerpt_start_point + Point::new(row_range.end - multibuffer_start.row, 0);
|
||||
buffer_end = excerpt.buffer.anchor_before(buffer_end_point);
|
||||
}
|
||||
|
||||
let buffer_hunks = excerpt
|
||||
.buffer
|
||||
.git_diff_hunks_intersecting_range(buffer_start..buffer_end)
|
||||
.filter_map(move |hunk| {
|
||||
let start = multibuffer_start.row
|
||||
+ hunk
|
||||
.buffer_range
|
||||
.start
|
||||
.saturating_sub(excerpt_start_point.row);
|
||||
let end = multibuffer_start.row
|
||||
+ hunk
|
||||
.buffer_range
|
||||
.end
|
||||
.min(excerpt_end_point.row + 1)
|
||||
.saturating_sub(excerpt_start_point.row);
|
||||
|
||||
Some(DiffHunk {
|
||||
buffer_range: start..end,
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.clone(),
|
||||
})
|
||||
});
|
||||
|
||||
cursor.next(&());
|
||||
|
||||
Some(buffer_hunks)
|
||||
})
|
||||
.flatten()
|
||||
@@ -2886,7 +3026,7 @@ impl MultiBufferSnapshot {
|
||||
&self,
|
||||
offset: T,
|
||||
theme: Option<&SyntaxTheme>,
|
||||
) -> Option<(usize, Vec<OutlineItem<Anchor>>)> {
|
||||
) -> Option<(u64, Vec<OutlineItem<Anchor>>)> {
|
||||
let anchor = self.anchor_before(offset);
|
||||
let excerpt_id = anchor.excerpt_id();
|
||||
let excerpt = self.excerpt(excerpt_id)?;
|
||||
@@ -2926,7 +3066,7 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<usize> {
|
||||
pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<u64> {
|
||||
Some(self.excerpt(excerpt_id)?.buffer_id)
|
||||
}
|
||||
|
||||
@@ -3064,7 +3204,7 @@ impl History {
|
||||
fn end_transaction(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
buffer_transactions: HashMap<usize, TransactionId>,
|
||||
buffer_transactions: HashMap<u64, TransactionId>,
|
||||
) -> bool {
|
||||
assert_ne!(self.transaction_depth, 0);
|
||||
self.transaction_depth -= 1;
|
||||
@@ -3089,8 +3229,12 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
fn push_transaction<'a, T>(&mut self, buffer_transactions: T, now: Instant)
|
||||
where
|
||||
fn push_transaction<'a, T>(
|
||||
&mut self,
|
||||
buffer_transactions: T,
|
||||
now: Instant,
|
||||
cx: &mut ModelContext<MultiBuffer>,
|
||||
) where
|
||||
T: IntoIterator<Item = (&'a ModelHandle<Buffer>, &'a language::Transaction)>,
|
||||
{
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
@@ -3098,7 +3242,7 @@ impl History {
|
||||
id: self.next_transaction_id.tick(),
|
||||
buffer_transactions: buffer_transactions
|
||||
.into_iter()
|
||||
.map(|(buffer, transaction)| (buffer.id(), transaction.id))
|
||||
.map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id))
|
||||
.collect(),
|
||||
first_edit_at: now,
|
||||
last_edit_at: now,
|
||||
@@ -3195,7 +3339,7 @@ impl Excerpt {
|
||||
fn new(
|
||||
id: ExcerptId,
|
||||
locator: Locator,
|
||||
buffer_id: usize,
|
||||
buffer_id: u64,
|
||||
buffer: BufferSnapshot,
|
||||
range: ExcerptRange<text::Anchor>,
|
||||
has_trailing_newline: bool,
|
||||
@@ -3724,15 +3868,17 @@ where
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::editor_tests::init_test;
|
||||
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use language::{Buffer, Rope};
|
||||
use project::{FakeFs, Project};
|
||||
use rand::prelude::*;
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use std::{env, rc::Rc};
|
||||
use unindent::Unindent;
|
||||
|
||||
use util::test::sample_text;
|
||||
|
||||
#[gpui::test]
|
||||
@@ -4024,19 +4170,25 @@ mod tests {
|
||||
|
||||
let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let follower_edit_event_count = Rc::new(RefCell::new(0));
|
||||
|
||||
follower_multibuffer.update(cx, |_, cx| {
|
||||
cx.subscribe(&leader_multibuffer, |follower, _, event, cx| {
|
||||
match event.clone() {
|
||||
let follower_edit_event_count = follower_edit_event_count.clone();
|
||||
cx.subscribe(
|
||||
&leader_multibuffer,
|
||||
move |follower, _, event, cx| match event.clone() {
|
||||
Event::ExcerptsAdded {
|
||||
buffer,
|
||||
predecessor,
|
||||
excerpts,
|
||||
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
|
||||
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
|
||||
Event::Edited => {
|
||||
*follower_edit_event_count.borrow_mut() += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
});
|
||||
|
||||
@@ -4075,6 +4227,7 @@ mod tests {
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 2);
|
||||
|
||||
leader_multibuffer.update(cx, |leader, cx| {
|
||||
let excerpt_ids = leader.excerpt_ids();
|
||||
@@ -4084,6 +4237,27 @@ mod tests {
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 3);
|
||||
|
||||
// Removing an empty set of excerpts is a noop.
|
||||
leader_multibuffer.update(cx, |leader, cx| {
|
||||
leader.remove_excerpts([], cx);
|
||||
});
|
||||
assert_eq!(
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 3);
|
||||
|
||||
// Adding an empty set of excerpts is a noop.
|
||||
leader_multibuffer.update(cx, |leader, cx| {
|
||||
leader.push_excerpts::<usize>(buffer_2.clone(), [], cx);
|
||||
});
|
||||
assert_eq!(
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 3);
|
||||
|
||||
leader_multibuffer.update(cx, |leader, cx| {
|
||||
leader.clear(cx);
|
||||
@@ -4092,6 +4266,7 @@ mod tests {
|
||||
leader_multibuffer.read(cx).snapshot(cx).text(),
|
||||
follower_multibuffer.read(cx).snapshot(cx).text(),
|
||||
);
|
||||
assert_eq!(*follower_edit_event_count.borrow(), 4);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -4390,73 +4565,85 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
|
||||
use git::diff::DiffHunkStatus;
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
// buffer has two modified hunks with two rows each
|
||||
let buffer_1 = cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(
|
||||
0,
|
||||
"
|
||||
1.zero
|
||||
1.ONE
|
||||
1.TWO
|
||||
1.three
|
||||
1.FOUR
|
||||
1.FIVE
|
||||
1.six
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
let buffer_1 = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_buffer(
|
||||
"
|
||||
1.zero
|
||||
1.ONE
|
||||
1.TWO
|
||||
1.three
|
||||
1.FOUR
|
||||
1.FIVE
|
||||
1.six
|
||||
"
|
||||
.unindent()
|
||||
.as_str(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
buffer_1.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(
|
||||
Some(
|
||||
"
|
||||
1.zero
|
||||
1.one
|
||||
1.two
|
||||
1.three
|
||||
1.four
|
||||
1.five
|
||||
1.six
|
||||
"
|
||||
1.zero
|
||||
1.one
|
||||
1.two
|
||||
1.three
|
||||
1.four
|
||||
1.five
|
||||
1.six
|
||||
"
|
||||
.unindent(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
buffer
|
||||
});
|
||||
|
||||
// buffer has a deletion hunk and an insertion hunk
|
||||
let buffer_2 = cx.add_model(|cx| {
|
||||
let mut buffer = Buffer::new(
|
||||
0,
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.five
|
||||
2.six
|
||||
"
|
||||
.unindent(),
|
||||
cx,
|
||||
);
|
||||
let buffer_2 = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_buffer(
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.five
|
||||
2.six
|
||||
"
|
||||
.unindent()
|
||||
.as_str(),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
buffer_2.update(cx, |buffer, cx| {
|
||||
buffer.set_diff_base(
|
||||
Some(
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.one-and-a-half
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.six
|
||||
"
|
||||
2.zero
|
||||
2.one
|
||||
2.one-and-a-half
|
||||
2.two
|
||||
2.three
|
||||
2.four
|
||||
2.six
|
||||
"
|
||||
.unindent(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
buffer
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
@@ -4539,7 +4726,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range(0..12, false)
|
||||
.git_diff_hunks_in_range(0..12)
|
||||
.map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
.collect::<Vec<_>>(),
|
||||
&expected,
|
||||
@@ -4547,7 +4734,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.git_diff_hunks_in_range(0..12, true)
|
||||
.git_diff_hunks_in_range_rev(0..12)
|
||||
.map(|hunk| (hunk.status(), hunk.buffer_range))
|
||||
.collect::<Vec<_>>(),
|
||||
expected
|
||||
@@ -4663,7 +4850,7 @@ mod tests {
|
||||
"Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}",
|
||||
excerpt_ix,
|
||||
expected_excerpts.len(),
|
||||
buffer_handle.id(),
|
||||
buffer_handle.read(cx).remote_id(),
|
||||
buffer.text(),
|
||||
start_ix..end_ix,
|
||||
&buffer.text()[start_ix..end_ix]
|
||||
@@ -4749,8 +4936,8 @@ mod tests {
|
||||
|
||||
let mut excerpt_starts = excerpt_starts.into_iter();
|
||||
for (buffer, range) in &expected_excerpts {
|
||||
let buffer_id = buffer.id();
|
||||
let buffer = buffer.read(cx);
|
||||
let buffer_id = buffer.remote_id();
|
||||
let buffer_range = range.to_offset(buffer);
|
||||
let buffer_start_point = buffer.offset_to_point(buffer_range.start);
|
||||
let buffer_start_point_utf16 =
|
||||
@@ -4902,16 +5089,19 @@ mod tests {
|
||||
.read(cx)
|
||||
.range_to_buffer_ranges(start_ix..end_ix, cx);
|
||||
let excerpted_buffers_text = excerpted_buffer_ranges
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|(buffer, buffer_range)| {
|
||||
buffer
|
||||
.read(cx)
|
||||
.text_for_range(buffer_range)
|
||||
.text_for_range(buffer_range.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert_eq!(excerpted_buffers_text, text_for_range);
|
||||
if !expected_excerpts.is_empty() {
|
||||
assert!(!excerpted_buffer_ranges.is_empty());
|
||||
}
|
||||
|
||||
let expected_summary = TextSummary::from(&expected_text[start_ix..end_ix]);
|
||||
assert_eq!(
|
||||
@@ -4978,7 +5168,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_history(cx: &mut AppContext) {
|
||||
cx.set_global(Settings::test(cx));
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
|
||||
@@ -8,7 +8,7 @@ use sum_tree::Bias;
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash)]
|
||||
pub struct Anchor {
|
||||
pub(crate) buffer_id: Option<usize>,
|
||||
pub(crate) buffer_id: Option<u64>,
|
||||
pub(crate) excerpt_id: ExcerptId,
|
||||
pub(crate) text_anchor: text::Anchor,
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use workspace::WorkspaceId;
|
||||
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::{hide_hover, HideHover},
|
||||
hover_popover::hide_hover,
|
||||
persistence::DB,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
|
||||
};
|
||||
@@ -245,14 +245,14 @@ impl ScrollManager {
|
||||
}
|
||||
|
||||
if cx.default_global::<ScrollbarAutoHide>().0 {
|
||||
self.hide_scrollbar_task = Some(cx.spawn_weak(|editor, mut cx| async move {
|
||||
self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move {
|
||||
cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await;
|
||||
if let Some(editor) = editor.upgrade(&cx) {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
editor.scroll_manager.show_scrollbars = false;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
} else {
|
||||
self.hide_scrollbar_task = None;
|
||||
@@ -307,14 +307,10 @@ impl Editor {
|
||||
) {
|
||||
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
hide_hover(self, &HideHover, cx);
|
||||
self.scroll_manager.set_scroll_position(
|
||||
scroll_position,
|
||||
&map,
|
||||
local,
|
||||
self.workspace_id,
|
||||
cx,
|
||||
);
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
self.scroll_manager
|
||||
.set_scroll_position(scroll_position, &map, local, workspace_id, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||
@@ -323,13 +319,14 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
|
||||
hide_hover(self, &HideHover, cx);
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
let top_row = scroll_anchor
|
||||
.top_anchor
|
||||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
self.scroll_manager
|
||||
.set_anchor(scroll_anchor, top_row, true, self.workspace_id, cx);
|
||||
.set_anchor(scroll_anchor, top_row, true, workspace_id, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_anchor_remote(
|
||||
@@ -337,13 +334,14 @@ impl Editor {
|
||||
scroll_anchor: ScrollAnchor,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
hide_hover(self, &HideHover, cx);
|
||||
hide_hover(self, cx);
|
||||
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
|
||||
let top_row = scroll_anchor
|
||||
.top_anchor
|
||||
.to_point(&self.buffer().read(cx).snapshot(cx))
|
||||
.row;
|
||||
self.scroll_manager
|
||||
.set_anchor(scroll_anchor, top_row, false, self.workspace_id, cx);
|
||||
.set_anchor(scroll_anchor, top_row, false, workspace_id, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use gpui::{
|
||||
actions, geometry::vector::Vector2F, impl_internal_actions, AppContext, Axis, ViewContext,
|
||||
};
|
||||
use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext};
|
||||
use language::Bias;
|
||||
|
||||
use crate::{Editor, EditorMode};
|
||||
@@ -23,17 +21,8 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Scroll {
|
||||
pub scroll_position: Vector2F,
|
||||
pub axis: Option<Axis>,
|
||||
}
|
||||
|
||||
impl_internal_actions!(editor, [Scroll]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(Editor::next_screen);
|
||||
cx.add_action(Editor::scroll);
|
||||
cx.add_action(Editor::scroll_cursor_top);
|
||||
cx.add_action(Editor::scroll_cursor_center);
|
||||
cx.add_action(Editor::scroll_cursor_bottom);
|
||||
@@ -75,9 +64,14 @@ impl Editor {
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
|
||||
self.scroll_manager.update_ongoing_scroll(action.axis);
|
||||
self.set_scroll_position(action.scroll_position, cx);
|
||||
pub fn scroll(
|
||||
&mut self,
|
||||
scroll_position: Vector2F,
|
||||
axis: Option<Axis>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.scroll_manager.update_ongoing_scroll(axis);
|
||||
self.set_scroll_position(scroll_position, cx);
|
||||
}
|
||||
|
||||
fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::cmp;
|
||||
|
||||
use gpui::{text_layout, ViewContext};
|
||||
use gpui::ViewContext;
|
||||
use language::Point;
|
||||
|
||||
use crate::{display_map::ToDisplayPoint, Editor, EditorMode};
|
||||
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Autoscroll {
|
||||
@@ -172,7 +172,7 @@ impl Editor {
|
||||
viewport_width: f32,
|
||||
scroll_width: f32,
|
||||
max_glyph_width: f32,
|
||||
layouts: &[text_layout::Line],
|
||||
layouts: &[LineWithInvisibles],
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -194,10 +194,13 @@ impl Editor {
|
||||
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
|
||||
target_left = target_left.min(
|
||||
layouts[(head.row() - start_row) as usize]
|
||||
.line
|
||||
.x_for_index(start_column as usize),
|
||||
);
|
||||
target_right = target_right.max(
|
||||
layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize)
|
||||
layouts[(head.row() - start_row) as usize]
|
||||
.line
|
||||
.x_for_index(end_column as usize)
|
||||
+ max_glyph_width,
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user