Compare commits
797 Commits
streaming-
...
variable-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db415462c2 | ||
|
|
0971042cac | ||
|
|
da89fdfd5e | ||
|
|
c4568078e7 | ||
|
|
3c4acf045e | ||
|
|
d325d4bda4 | ||
|
|
e257403262 | ||
|
|
8b59056b1e | ||
|
|
2af93fe609 | ||
|
|
0797e2a97a | ||
|
|
6021f04a59 | ||
|
|
fa3cf3e1df | ||
|
|
62f2bc8a8d | ||
|
|
17c7ede60c | ||
|
|
f95c4df237 | ||
|
|
d50d3e4342 | ||
|
|
393fe5f71f | ||
|
|
f8eebc73f5 | ||
|
|
43bf537bba | ||
|
|
2dfe6cdffc | ||
|
|
bae466e4b5 | ||
|
|
0eee9a45e5 | ||
|
|
86f636f5dc | ||
|
|
656761bda8 | ||
|
|
82b025831f | ||
|
|
7b3c2ac229 | ||
|
|
e5a64cacf9 | ||
|
|
aa954eb1ab | ||
|
|
49e94f6ff2 | ||
|
|
d14b02272d | ||
|
|
cb2fda0f3f | ||
|
|
0a59016961 | ||
|
|
12cead7bc9 | ||
|
|
f4bd652745 | ||
|
|
cba76fe29b | ||
|
|
9a7ecdc224 | ||
|
|
6d2f113528 | ||
|
|
f06204ae45 | ||
|
|
cc5bcd80b1 | ||
|
|
63e2ca3335 | ||
|
|
f12b3c9147 | ||
|
|
3c65060606 | ||
|
|
d05b4905c5 | ||
|
|
66d6b794a1 | ||
|
|
f7595a07a0 | ||
|
|
37b29e11ee | ||
|
|
5b470a6870 | ||
|
|
2bd56f7d02 | ||
|
|
7695b73b71 | ||
|
|
54858fbf53 | ||
|
|
9cd0ef225a | ||
|
|
2a802d4f80 | ||
|
|
ed82419303 | ||
|
|
1dc7c412f6 | ||
|
|
a30b50c1dc | ||
|
|
8bfa317171 | ||
|
|
e529783d5a | ||
|
|
6a339a1193 | ||
|
|
bd3ffd7150 | ||
|
|
255c86c6f0 | ||
|
|
417e1d9907 | ||
|
|
a8c79cce99 | ||
|
|
7b49c7aad6 | ||
|
|
104980ac15 | ||
|
|
14472db13c | ||
|
|
c391ff8742 | ||
|
|
7b0dd6827c | ||
|
|
76b2004489 | ||
|
|
f957e79e5d | ||
|
|
d34726f285 | ||
|
|
97ed70db92 | ||
|
|
ef87cc3fb3 | ||
|
|
0e998cde84 | ||
|
|
0c7cd6f848 | ||
|
|
d327e6015d | ||
|
|
38a634c536 | ||
|
|
6375d34f50 | ||
|
|
58640da34b | ||
|
|
597371960f | ||
|
|
3378b7a859 | ||
|
|
eeded3d0a4 | ||
|
|
9bac1ff9a2 | ||
|
|
6e17f91bb6 | ||
|
|
9815a8f4b6 | ||
|
|
0358cdc562 | ||
|
|
55a39d5358 | ||
|
|
26502cdd9e | ||
|
|
c8632a3323 | ||
|
|
f4cb78f5c6 | ||
|
|
03144d7792 | ||
|
|
8a5e75408b | ||
|
|
2e83b375a2 | ||
|
|
14eab34e61 | ||
|
|
c711168ad4 | ||
|
|
b43a2d1a8a | ||
|
|
650984bfe5 | ||
|
|
83a8933a27 | ||
|
|
13dd3aa2b5 | ||
|
|
ac68f31550 | ||
|
|
95eca602c4 | ||
|
|
1af8c11ef4 | ||
|
|
859cc67fa1 | ||
|
|
060c8df7e8 | ||
|
|
1644871336 | ||
|
|
c1ad9873db | ||
|
|
df6c3649df | ||
|
|
286532e439 | ||
|
|
4e0408ab34 | ||
|
|
5908d5d5fd | ||
|
|
4bb00ad953 | ||
|
|
4d2ecb2cdf | ||
|
|
b159e3c163 | ||
|
|
2ca2942ffa | ||
|
|
27bda501a1 | ||
|
|
b4c2de0838 | ||
|
|
1c10133f1e | ||
|
|
b8911725c2 | ||
|
|
9ea14ec74d | ||
|
|
083fca426e | ||
|
|
e82520c6cf | ||
|
|
1d5fc3771f | ||
|
|
04a1e569c2 | ||
|
|
ced8514177 | ||
|
|
c308a8cc2d | ||
|
|
a8f59b8a1d | ||
|
|
afaaf24455 | ||
|
|
b8383bedae | ||
|
|
bd27f879e2 | ||
|
|
05a35375a4 | ||
|
|
28d6e76298 | ||
|
|
abda28fb6b | ||
|
|
9b8ea48883 | ||
|
|
edfeea2c28 | ||
|
|
7797b7021d | ||
|
|
6737555f5f | ||
|
|
16dba67b38 | ||
|
|
45cf5b5ab5 | ||
|
|
c07526f30b | ||
|
|
24e816be38 | ||
|
|
b67a382e57 | ||
|
|
06338963f3 | ||
|
|
95590967eb | ||
|
|
9786da2fe5 | ||
|
|
0dc90fd35e | ||
|
|
1e0a0fa9f9 | ||
|
|
f81463cc93 | ||
|
|
c9a9ab22f0 | ||
|
|
cf13a62bb5 | ||
|
|
fbf4eb9472 | ||
|
|
ad356d9c8e | ||
|
|
45db28a5ce | ||
|
|
a0c91d620a | ||
|
|
b1ca8c3e45 | ||
|
|
f8e248286e | ||
|
|
0233152bd1 | ||
|
|
294ce96b24 | ||
|
|
f7886adf9a | ||
|
|
bc5904e485 | ||
|
|
12c02a12ca | ||
|
|
36b430a1ae | ||
|
|
7fe8c626d3 | ||
|
|
43e2c4de06 | ||
|
|
6648d6299f | ||
|
|
d0f64d475d | ||
|
|
e285ae60c3 | ||
|
|
53336eedb7 | ||
|
|
3aab70c7bb | ||
|
|
cdb5af2906 | ||
|
|
936ac212e7 | ||
|
|
debcb1f26f | ||
|
|
90440d9652 | ||
|
|
be80dedd8c | ||
|
|
00c24ce289 | ||
|
|
c820f12e9c | ||
|
|
3833788cbc | ||
|
|
91e60d79bb | ||
|
|
ecfc0ef12d | ||
|
|
661e5b0335 | ||
|
|
041e1eef38 | ||
|
|
c8ba6d7c57 | ||
|
|
9d5525e0fb | ||
|
|
9ac18e9404 | ||
|
|
90deb4a82e | ||
|
|
8b45634d39 | ||
|
|
e216805e66 | ||
|
|
062e64d227 | ||
|
|
39d4d624fb | ||
|
|
dad39abeeb | ||
|
|
05ca096a5b | ||
|
|
2fc7c17497 | ||
|
|
af77b2f99f | ||
|
|
16eff80a7b | ||
|
|
64bc112fe5 | ||
|
|
9ef8aceb81 | ||
|
|
2973b46c78 | ||
|
|
b6182d0781 | ||
|
|
dac28730e5 | ||
|
|
9c3e85a6ff | ||
|
|
e21ea19fc6 | ||
|
|
cee4e25d51 | ||
|
|
33e39281a6 | ||
|
|
da66f75410 | ||
|
|
b7f0a1f5c8 | ||
|
|
6ec4adffe7 | ||
|
|
180ce5e9ba | ||
|
|
4fcb10dd26 | ||
|
|
1be2836f60 | ||
|
|
c76144e918 | ||
|
|
aa268d1379 | ||
|
|
3cf96588e2 | ||
|
|
27b60436b8 | ||
|
|
28874b60cf | ||
|
|
dcde289f94 | ||
|
|
52350245e6 | ||
|
|
a56e3eafdf | ||
|
|
ad98bf444d | ||
|
|
080d0c46ad | ||
|
|
dfeddaae8a | ||
|
|
5cd93ca158 | ||
|
|
d60089888f | ||
|
|
4c930652af | ||
|
|
44e9444c8c | ||
|
|
69548f5f34 | ||
|
|
c45b6e9271 | ||
|
|
26f14fd036 | ||
|
|
945e3226d8 | ||
|
|
3bf5833135 | ||
|
|
47b3f55a17 | ||
|
|
c994075327 | ||
|
|
b6b7ad38b5 | ||
|
|
e81a7e1e06 | ||
|
|
889949ca76 | ||
|
|
ef3a6deb05 | ||
|
|
197166a8c1 | ||
|
|
dfe978b06a | ||
|
|
a2e0b6778b | ||
|
|
25db814d0c | ||
|
|
78865820d8 | ||
|
|
1fb0c5b84e | ||
|
|
ec547b8ca3 | ||
|
|
56abc60a34 | ||
|
|
a89e29554f | ||
|
|
8ceb115f3c | ||
|
|
953843acdd | ||
|
|
5a52c7e21f | ||
|
|
9bca023b1a | ||
|
|
647f411b10 | ||
|
|
e1392c9717 | ||
|
|
7b6d20b9ff | ||
|
|
675fb2f54b | ||
|
|
1a5feff9ed | ||
|
|
4de9921fa8 | ||
|
|
5ecfef0aa0 | ||
|
|
868f55c074 | ||
|
|
272721ae30 | ||
|
|
f34fc7fbe8 | ||
|
|
a193efd65a | ||
|
|
568f127f43 | ||
|
|
bc49c18481 | ||
|
|
d03e4149ea | ||
|
|
86946506bf | ||
|
|
41b702807d | ||
|
|
a9d7858f30 | ||
|
|
06c11f97bb | ||
|
|
8ecd5479ac | ||
|
|
a7e26bbfb5 | ||
|
|
f9f28107f5 | ||
|
|
2736b2f477 | ||
|
|
5aa816e85b | ||
|
|
4baa7f7742 | ||
|
|
4e13a00c44 | ||
|
|
943609f1ac | ||
|
|
887e2a65e1 | ||
|
|
c91f51dd68 | ||
|
|
797f5bd967 | ||
|
|
14a2f7526b | ||
|
|
918869fcfe | ||
|
|
46b72f6e3b | ||
|
|
73627a3843 | ||
|
|
fa17737332 | ||
|
|
01648b9370 | ||
|
|
dd4a1f7d30 | ||
|
|
8911c96399 | ||
|
|
404f77357d | ||
|
|
d99653fd15 | ||
|
|
72f13890f0 | ||
|
|
2551a0fe13 | ||
|
|
501073393c | ||
|
|
78d342d582 | ||
|
|
f2722db366 | ||
|
|
f0cd2bfa61 | ||
|
|
020623a19d | ||
|
|
e551923477 | ||
|
|
cbbf2daa2f | ||
|
|
9497987438 | ||
|
|
331aafde23 | ||
|
|
f9c88fb50f | ||
|
|
1698965317 | ||
|
|
1c59d2203b | ||
|
|
42d1b484bd | ||
|
|
7223bf93ba | ||
|
|
3afda611b4 | ||
|
|
acb3ee23a8 | ||
|
|
dd5083ad62 | ||
|
|
0b68944397 | ||
|
|
f0947c1197 | ||
|
|
9cd7e072fe | ||
|
|
f682077ffd | ||
|
|
b3aa18d70c | ||
|
|
030ee04e54 | ||
|
|
e2206b876b | ||
|
|
1f0304f002 | ||
|
|
b5b877bd26 | ||
|
|
bfdf12c51c | ||
|
|
e0891a1df7 | ||
|
|
c1e15c0907 | ||
|
|
f4669e8965 | ||
|
|
43defed0a4 | ||
|
|
ea18dfbe94 | ||
|
|
7ced61d798 | ||
|
|
fba00c728e | ||
|
|
ca970dd77d | ||
|
|
569f500b52 | ||
|
|
b911fb1c9a | ||
|
|
b8c89d3d8a | ||
|
|
098bc759a9 | ||
|
|
ccacce9ccb | ||
|
|
1929dec4a0 | ||
|
|
ed7443db6e | ||
|
|
3e9139eeb1 | ||
|
|
ad1e51f64a | ||
|
|
a91faaceec | ||
|
|
cea5cc7cd0 | ||
|
|
f6d26afb13 | ||
|
|
f8fe1652a7 | ||
|
|
39e70354c1 | ||
|
|
4f8c19a93a | ||
|
|
1b1d37484b | ||
|
|
5dbadab1ac | ||
|
|
d7fa7c208d | ||
|
|
6aba39874c | ||
|
|
5fe2da3e72 | ||
|
|
ab4973fbdb | ||
|
|
e3cb3d143e | ||
|
|
6ed42285d6 | ||
|
|
0b97ffad13 | ||
|
|
7f4f7b056e | ||
|
|
473ebbb6c3 | ||
|
|
3474750588 | ||
|
|
28c6012ff8 | ||
|
|
9a802b9133 | ||
|
|
d7b5132bd9 | ||
|
|
65789d3925 | ||
|
|
bea29464f9 | ||
|
|
980f9c7353 | ||
|
|
aefef9c58c | ||
|
|
29a9eaf5fc | ||
|
|
490d42599b | ||
|
|
04cd04eb44 | ||
|
|
22e52c306a | ||
|
|
6a4a285ee6 | ||
|
|
a50f3c36b0 | ||
|
|
1a0ecf0c16 | ||
|
|
2b8ae367c7 | ||
|
|
61e8d0c39b | ||
|
|
386031e6dd | ||
|
|
b8b65f7a8f | ||
|
|
4068960686 | ||
|
|
df71b972e1 | ||
|
|
6bc5679c86 | ||
|
|
35a52a7f90 | ||
|
|
5971d37942 | ||
|
|
046ff02062 | ||
|
|
2c45c57f7b | ||
|
|
8e738ba4d5 | ||
|
|
008bd534af | ||
|
|
8c72b99031 | ||
|
|
7e0150790a | ||
|
|
a3220dc31d | ||
|
|
3aaee14ecc | ||
|
|
0d84881641 | ||
|
|
b6dc3ca86f | ||
|
|
99bfc342ab | ||
|
|
3a77d7a655 | ||
|
|
2c1f348c49 | ||
|
|
d8f8140965 | ||
|
|
ae0d08f36c | ||
|
|
23ccf08da1 | ||
|
|
977fd87a51 | ||
|
|
80f775e186 | ||
|
|
6f0e223dc7 | ||
|
|
ca80d0c3bd | ||
|
|
ce30deac63 | ||
|
|
31e3e48052 | ||
|
|
81ca004ee6 | ||
|
|
15dd1ee22e | ||
|
|
c54454fa42 | ||
|
|
964a6e8585 | ||
|
|
aa5d51d776 | ||
|
|
ba25aa26c9 | ||
|
|
b69d031e15 | ||
|
|
68ee3c747a | ||
|
|
7cec577daa | ||
|
|
0976e85eb0 | ||
|
|
c19c86085b | ||
|
|
055ffc17cd | ||
|
|
b728779a9a | ||
|
|
fb5bee3ba8 | ||
|
|
ffaaadf2b9 | ||
|
|
9781292cba | ||
|
|
9e37b4708f | ||
|
|
bae6edba88 | ||
|
|
9772573816 | ||
|
|
56f77c3192 | ||
|
|
56df4fbe6e | ||
|
|
45c4aef0da | ||
|
|
1b2871aac2 | ||
|
|
65cd774baa | ||
|
|
591f6cc9a2 | ||
|
|
bb89c1d913 | ||
|
|
d1ddbfc586 | ||
|
|
d226de3230 | ||
|
|
5efdaf3ca7 | ||
|
|
cd2b4a8714 | ||
|
|
15535d3cec | ||
|
|
747ef3e7ec | ||
|
|
aa9875277a | ||
|
|
0c647ae071 | ||
|
|
f6556c5bb6 | ||
|
|
f7eb5213a5 | ||
|
|
d279afa41c | ||
|
|
ddaf1508d3 | ||
|
|
96871b493f | ||
|
|
a45aa3d014 | ||
|
|
57668dbaeb | ||
|
|
115f2eb2e2 | ||
|
|
290c76daef | ||
|
|
e2d449a11f | ||
|
|
a95b16aa67 | ||
|
|
932f4ed10a | ||
|
|
61daad2377 | ||
|
|
8baa2805d0 | ||
|
|
ffc058209d | ||
|
|
b426ff6074 | ||
|
|
6f973bdda4 | ||
|
|
43ea3b47a4 | ||
|
|
30a4c84a48 | ||
|
|
211fd50776 | ||
|
|
fc78c40385 | ||
|
|
6735cfad67 | ||
|
|
2e028a7038 | ||
|
|
afe228fd77 | ||
|
|
fdea7d9de9 | ||
|
|
1c1e34b3d2 | ||
|
|
3a6f2adcc6 | ||
|
|
13afc3741f | ||
|
|
c65ed1c738 | ||
|
|
f5dc1175b8 | ||
|
|
5758f664bc | ||
|
|
b46b8aa76a | ||
|
|
222cd4ba43 | ||
|
|
5bb7f2408a | ||
|
|
b1d24a0524 | ||
|
|
177ae28ab2 | ||
|
|
554a402cec | ||
|
|
7e2c1386fc | ||
|
|
8a4f677119 | ||
|
|
a728f9e751 | ||
|
|
171c74223c | ||
|
|
5f1de1ab65 | ||
|
|
91926cdcfe | ||
|
|
08935b29ef | ||
|
|
eedd865ae8 | ||
|
|
00b6fdc098 | ||
|
|
187d909736 | ||
|
|
ac0ba07c61 | ||
|
|
984cb686a8 | ||
|
|
3b545a79ba | ||
|
|
f76b7c9337 | ||
|
|
8c0a7b1024 | ||
|
|
c2ed56af0b | ||
|
|
f9b045bd23 | ||
|
|
3c301c3ea4 | ||
|
|
93af1bfae7 | ||
|
|
6b4ebac822 | ||
|
|
55c65700ad | ||
|
|
842bf0287d | ||
|
|
1914cef0aa | ||
|
|
171ddfb554 | ||
|
|
bfddc634be | ||
|
|
29918d96a2 | ||
|
|
3b3ac85199 | ||
|
|
9bc08f9f84 | ||
|
|
ce77773796 | ||
|
|
9016a03e90 | ||
|
|
a3dff431c5 | ||
|
|
231b5a910a | ||
|
|
7dec58ce89 | ||
|
|
8b96ac8138 | ||
|
|
4ddb65bdaa | ||
|
|
c26a8f1537 | ||
|
|
f1f1426635 | ||
|
|
278699f2f7 | ||
|
|
9612b60ccb | ||
|
|
c88316655c | ||
|
|
c3a778724f | ||
|
|
c1ab059d54 | ||
|
|
142a6dea17 | ||
|
|
e7f7fb759d | ||
|
|
621d1812d1 | ||
|
|
8cdb1fb55a | ||
|
|
716a81756f | ||
|
|
56943e2c78 | ||
|
|
4694de8e8a | ||
|
|
3985963259 | ||
|
|
559173e550 | ||
|
|
9b82278bb1 | ||
|
|
4405ae2d19 | ||
|
|
571d99cecf | ||
|
|
ed6da4a7f6 | ||
|
|
8a835bcf88 | ||
|
|
3fac36bd77 | ||
|
|
7a6f9d9559 | ||
|
|
ab6f3341c3 | ||
|
|
a4ce44629c | ||
|
|
18fb45f526 | ||
|
|
3184ba1df0 | ||
|
|
e6049f9830 | ||
|
|
0f5e5ea3b4 | ||
|
|
5a0c7d2885 | ||
|
|
499024297d | ||
|
|
3683920dab | ||
|
|
5d07ab0d03 | ||
|
|
09c195e78e | ||
|
|
165e058dce | ||
|
|
0e6042e12f | ||
|
|
1e99694f29 | ||
|
|
dc5d0f4148 | ||
|
|
b2927a07e4 | ||
|
|
b00d63b6c4 | ||
|
|
edf4e53571 | ||
|
|
ac2aa795cd | ||
|
|
b009832229 | ||
|
|
7b7a4757cd | ||
|
|
83cc452465 | ||
|
|
4cf735bd93 | ||
|
|
fc4078f8e8 | ||
|
|
243bd4b225 | ||
|
|
3ac4d1eaac | ||
|
|
921d0c54a3 | ||
|
|
008b6b591b | ||
|
|
2b504b3248 | ||
|
|
045f927b4c | ||
|
|
199b6657c6 | ||
|
|
31b27e1e85 | ||
|
|
5a301fc83a | ||
|
|
618d81a3de | ||
|
|
9bcd03b755 | ||
|
|
f3e7129479 | ||
|
|
149116e76f | ||
|
|
bbb449c1b7 | ||
|
|
651c31c338 | ||
|
|
3c98e8986f | ||
|
|
7885da1a2c | ||
|
|
2843a36853 | ||
|
|
5c45e459bd | ||
|
|
fb169af400 | ||
|
|
db8b8beb70 | ||
|
|
1ac97d2e87 | ||
|
|
11b2bc1ffc | ||
|
|
da1fdd25cd | ||
|
|
e9f0e936ea | ||
|
|
3a3f4990ba | ||
|
|
bb40689472 | ||
|
|
9643e71947 | ||
|
|
9ea23b0a62 | ||
|
|
b561c68d28 | ||
|
|
26e9843a2a | ||
|
|
5678469f9d | ||
|
|
fe58a702a0 | ||
|
|
30b59a6c92 | ||
|
|
0f1738c8cc | ||
|
|
d3fe698c23 | ||
|
|
cb52082821 | ||
|
|
a4db59cabb | ||
|
|
7d243ac274 | ||
|
|
5a08fc2033 | ||
|
|
111a0dc539 | ||
|
|
1ad8ed77e4 | ||
|
|
c92ecc690f | ||
|
|
a4cc28f480 | ||
|
|
f4af5afe62 | ||
|
|
49dd57d1f9 | ||
|
|
96bdeca2ac | ||
|
|
ef63ccff66 | ||
|
|
4181c39224 | ||
|
|
f3cb4a467b | ||
|
|
5f4affdefe | ||
|
|
b1d830cb09 | ||
|
|
e24e5f7e89 | ||
|
|
68158d3fc5 | ||
|
|
0247fd6087 | ||
|
|
d4904f97bb | ||
|
|
8b63c1ab6f | ||
|
|
9dfd2f5bdd | ||
|
|
ca844637f7 | ||
|
|
42aefb4034 | ||
|
|
e974ddedce | ||
|
|
a82d759940 | ||
|
|
a0123e8557 | ||
|
|
7ff1a08356 | ||
|
|
c99865b853 | ||
|
|
fc4d46ec22 | ||
|
|
1c98c1c302 | ||
|
|
aa257ece86 | ||
|
|
b9c1e511a9 | ||
|
|
2ab7f834b2 | ||
|
|
26a5770e08 | ||
|
|
d6dd59c83f | ||
|
|
655b23c635 | ||
|
|
620e65411b | ||
|
|
d222fbe84c | ||
|
|
74931bd472 | ||
|
|
4c5deb0b4e | ||
|
|
a545400534 | ||
|
|
12c853e3f0 | ||
|
|
4bb8ec96fd | ||
|
|
08dbf365bb | ||
|
|
c39c0a55f5 | ||
|
|
4373e479f7 | ||
|
|
7f8c28877f | ||
|
|
1ff23477de | ||
|
|
d28950c633 | ||
|
|
6ff5e00740 | ||
|
|
b70acdfa4a | ||
|
|
403ae10087 | ||
|
|
9a8a54109e | ||
|
|
f0a5775204 | ||
|
|
916150a8e0 | ||
|
|
61533d737c | ||
|
|
11c740b47a | ||
|
|
9c2b909df5 | ||
|
|
8b05b88ada | ||
|
|
0975ca844c | ||
|
|
a136b7279a | ||
|
|
7f1bd3b1d9 | ||
|
|
c7f7d18681 | ||
|
|
96d3f13937 | ||
|
|
1a421db92d | ||
|
|
9c60884771 | ||
|
|
ee3323d12a | ||
|
|
b88fd3e0c5 | ||
|
|
2e10853b34 | ||
|
|
780bfafedf | ||
|
|
0b8c4ded9e | ||
|
|
15d5186399 | ||
|
|
9bc1d4a8ae | ||
|
|
7c9771b8d7 | ||
|
|
d08e28f4e0 | ||
|
|
ef67321ff2 | ||
|
|
4c777ad140 | ||
|
|
923ae5473a | ||
|
|
a48166e5a0 | ||
|
|
ef098c028b | ||
|
|
7ce1b8dc76 | ||
|
|
e350417a33 | ||
|
|
ea9e0755df | ||
|
|
6aced1b3aa | ||
|
|
99e01fc608 | ||
|
|
fe899c9164 | ||
|
|
f8b9937e51 | ||
|
|
dc5928374e | ||
|
|
0deb3cc606 | ||
|
|
ba4a70d7ae | ||
|
|
65a790e4ca | ||
|
|
18cb54280a | ||
|
|
b6e677eb06 | ||
|
|
ffa0609f8d | ||
|
|
77a314350f | ||
|
|
8d7ec33183 | ||
|
|
73ef771875 | ||
|
|
3e1aa65b20 | ||
|
|
92b93850bb | ||
|
|
737b03c928 | ||
|
|
1b42dd5865 | ||
|
|
c9074b1c25 | ||
|
|
00379280f3 | ||
|
|
4e2d0351cc | ||
|
|
a583efd9b9 | ||
|
|
6237c29a42 | ||
|
|
9ea9b41e73 | ||
|
|
8d99f9b7d2 | ||
|
|
014ffbce2e | ||
|
|
68dd3c90c2 | ||
|
|
4a6f6151f0 | ||
|
|
f108d4c705 | ||
|
|
5ab95f1e1a | ||
|
|
49da08ffa4 | ||
|
|
f287c897a4 | ||
|
|
d15ff2d06f | ||
|
|
648daa3237 | ||
|
|
33e127de09 | ||
|
|
5a9b279039 | ||
|
|
cce58570dc | ||
|
|
361bbec3a0 | ||
|
|
a87409813c | ||
|
|
da84aa1ac2 | ||
|
|
817760688a | ||
|
|
13e56010c1 | ||
|
|
b827a35e44 | ||
|
|
9006e8fdff | ||
|
|
ce8ec033f4 | ||
|
|
9678cc9bc3 | ||
|
|
e87c4ddadc | ||
|
|
3f8581a2fb | ||
|
|
00c5b83384 | ||
|
|
4aedc1cd0b | ||
|
|
d238675c1a | ||
|
|
dcf6f6ca30 | ||
|
|
f4606bd951 | ||
|
|
12bef0830a | ||
|
|
953a2b376c | ||
|
|
ac3b9f7a4c | ||
|
|
ef5990d427 | ||
|
|
23a81d5d70 | ||
|
|
515122c54d | ||
|
|
f4eacca987 | ||
|
|
003fb7c81e | ||
|
|
7d2f63ebbd | ||
|
|
93e0bbb833 | ||
|
|
8c5f6a0be7 | ||
|
|
d6cafb8315 | ||
|
|
a24b76b30b | ||
|
|
11d74ea4ec | ||
|
|
93f4775cf6 | ||
|
|
a93913b9a3 | ||
|
|
08afbc6b58 | ||
|
|
b869465f00 | ||
|
|
2debea8115 | ||
|
|
cae295ff65 | ||
|
|
c51206e980 | ||
|
|
1baa5aea94 | ||
|
|
3e022a5565 | ||
|
|
3dd769be94 | ||
|
|
7936a4bee3 | ||
|
|
09aabe481c | ||
|
|
2ea1e4fa85 | ||
|
|
8699dad0e3 | ||
|
|
47a5f0c620 | ||
|
|
854ff68bac | ||
|
|
01d384e676 | ||
|
|
ddd893a795 | ||
|
|
3d7cd5dac7 | ||
|
|
a6fdfb5191 | ||
|
|
73a68d560f | ||
|
|
b4eeb25f55 | ||
|
|
fc991ab273 | ||
|
|
ab58d14559 | ||
|
|
3a0b311378 | ||
|
|
b81065fe63 | ||
|
|
a67f28dba2 | ||
|
|
99b2472e83 | ||
|
|
be45d5aa73 | ||
|
|
0508df9e7b | ||
|
|
153efab377 | ||
|
|
8015fb70e3 | ||
|
|
79d23aa4fe | ||
|
|
d5dae425fc | ||
|
|
d9e09c4a66 | ||
|
|
331625e876 | ||
|
|
61949fb348 | ||
|
|
c7f4e09496 | ||
|
|
5442e116ce | ||
|
|
11a4fc8b02 | ||
|
|
d303ebd46e | ||
|
|
e1de8dc50e | ||
|
|
5fe110c1dd | ||
|
|
89b203d03a | ||
|
|
0f4f8abbaa | ||
|
|
0d97e9e579 | ||
|
|
14b913fb4b | ||
|
|
9f1cd2bdb5 | ||
|
|
547c40e332 | ||
|
|
9cff6d5aa5 | ||
|
|
7e438bc1f3 | ||
|
|
944a52ce91 | ||
|
|
95a814ed41 | ||
|
|
6b9295b6c4 | ||
|
|
1128fce61a | ||
|
|
7c355fdb0f | ||
|
|
0e2a0b9edc | ||
|
|
c130f9c2f2 | ||
|
|
08300c6e90 | ||
|
|
7b71119094 | ||
|
|
c18db76862 | ||
|
|
c0dd152509 | ||
|
|
f402a4e5ce |
19
.zed/debug.json
Normal file
@@ -0,0 +1,19 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug Zed with LLDB",
|
||||
"adapter": "lldb",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug Zed with GDB",
|
||||
"adapter": "gdb",
|
||||
"program": "$ZED_WORKTREE_ROOT/target/debug/zed",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"initialize_args": {
|
||||
"stopAtBeginningOfMainSubprogram": true
|
||||
}
|
||||
}
|
||||
]
|
||||
126
Cargo.lock
generated
@@ -13,7 +13,6 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"project",
|
||||
"smallvec",
|
||||
"ui",
|
||||
@@ -2820,9 +2819,12 @@ dependencies = [
|
||||
"clock",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"context_server",
|
||||
"ctor",
|
||||
"dap",
|
||||
"dashmap 6.1.0",
|
||||
"debugger_ui",
|
||||
"derive_more",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
@@ -3750,6 +3752,67 @@ dependencies = [
|
||||
"syn 2.0.90",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dap"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compression",
|
||||
"async-pipe",
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"client",
|
||||
"collections",
|
||||
"dap-types",
|
||||
"env_logger 0.11.6",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"language",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"sysinfo",
|
||||
"task",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dap-types"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=bf5632dc19f806e8a435c9f04a4bfe7322badea2#bf5632dc19f806e8a435c9f04a4bfe7322badea2"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dap_adapters"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"dap",
|
||||
"gpui",
|
||||
"language",
|
||||
"paths",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sysinfo",
|
||||
"task",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
@@ -3823,6 +3886,59 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugger_tools"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dap",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "debugger_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"dap",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
"feature_flags",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"picker",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rpc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sum_tree",
|
||||
"sysinfo",
|
||||
"task",
|
||||
"tasks_ui",
|
||||
"terminal_view",
|
||||
"theme",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deepseek"
|
||||
version = "0.1.0"
|
||||
@@ -4123,6 +4239,7 @@ dependencies = [
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"ordered-float 2.10.1",
|
||||
"parking_lot",
|
||||
@@ -10178,6 +10295,8 @@ dependencies = [
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"env_logger 0.11.6",
|
||||
"fancy-regex 0.14.0",
|
||||
"fs",
|
||||
@@ -10189,6 +10308,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"http_client",
|
||||
"image",
|
||||
"indexmap",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
@@ -10837,6 +10957,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"auto_update",
|
||||
"dap",
|
||||
"editor",
|
||||
"extension_host",
|
||||
"file_finder",
|
||||
@@ -13356,6 +13477,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"sha2",
|
||||
"shellexpand 2.1.2",
|
||||
@@ -16760,6 +16882,8 @@ dependencies = [
|
||||
"component_preview",
|
||||
"copilot",
|
||||
"db",
|
||||
"debugger_tools",
|
||||
"debugger_ui",
|
||||
"diagnostics",
|
||||
"editor",
|
||||
"env_logger 0.11.6",
|
||||
|
||||
@@ -35,6 +35,10 @@ members = [
|
||||
"crates/context_server_settings",
|
||||
"crates/copilot",
|
||||
"crates/credentials_provider",
|
||||
"crates/dap",
|
||||
"crates/dap_adapters",
|
||||
"crates/debugger_tools",
|
||||
"crates/debugger_ui",
|
||||
"crates/db",
|
||||
"crates/deepseek",
|
||||
"crates/diagnostics",
|
||||
@@ -239,7 +243,11 @@ context_server = { path = "crates/context_server" }
|
||||
context_server_settings = { path = "crates/context_server_settings" }
|
||||
copilot = { path = "crates/copilot" }
|
||||
credentials_provider = { path = "crates/credentials_provider" }
|
||||
dap = { path = "crates/dap" }
|
||||
dap_adapters = { path = "crates/dap_adapters" }
|
||||
db = { path = "crates/db" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
debugger_tools = { path = "crates/debugger_tools" }
|
||||
deepseek = { path = "crates/deepseek" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
buffer_diff = { path = "crates/buffer_diff" }
|
||||
|
||||
1
assets/icons/debug.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>
|
||||
|
After Width: | Height: | Size: 615 B |
1
assets/icons/debug_breakpoint.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle"><circle cx="12" cy="12" r="10"/></svg>
|
||||
|
After Width: | Height: | Size: 257 B |
1
assets/icons/debug_continue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-step-forward"><line x1="6" x2="6" y1="4" y2="20"/><polygon points="10,4 20,12 10,20"/></svg>
|
||||
|
After Width: | Height: | Size: 295 B |
1
assets/icons/debug_disconnect.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-unplug"><path d="m19 5 3-3"/><path d="m2 22 3-3"/><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z"/><path d="M7.5 13.5 10 11"/><path d="M10.5 16.5 13 14"/><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 474 B |
1
assets/icons/debug_ignore_breakpoints.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-off"><path d="m2 2 20 20"/><path d="M8.35 2.69A10 10 0 0 1 21.3 15.65"/><path d="M19.08 19.08A10 10 0 1 1 4.92 4.92"/></svg>
|
||||
|
After Width: | Height: | Size: 334 B |
1
assets/icons/debug_log_breakpoint.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></svg>
|
||||
|
After Width: | Height: | Size: 275 B |
1
assets/icons/debug_pause.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pause"><rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/></svg>
|
||||
|
After Width: | Height: | Size: 313 B |
1
assets/icons/debug_restart.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
|
||||
|
After Width: | Height: | Size: 302 B |
1
assets/icons/debug_step_back.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo-dot"><path d="M21 17a9 9 0 0 0-15-6.7L3 13"/><path d="M3 7v6h6"/><circle cx="12" cy="17" r="1"/></svg>
|
||||
|
After Width: | Height: | Size: 310 B |
5
assets/icons/debug_step_into.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-from-dot">
|
||||
<path d="m5 15 7 7 7-7"/>
|
||||
<path d="M12 8v14"/>
|
||||
<circle cx="12" cy="3" r="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
5
assets/icons/debug_step_out.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-from-dot">
|
||||
<path d="m3 10 9-8 9 8"/>
|
||||
<path d="M12 17V2"/>
|
||||
<circle cx="12" cy="21" r="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 314 B |
5
assets/icons/debug_step_over.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-redo-dot">
|
||||
<circle cx="12" cy="17" r="1"/>
|
||||
<path d="M21 7v6h-6"/>
|
||||
<path d="M3 17a9 9 0 0 1 9-9 9 9 0 0 1 6 2.3l3 2.7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 335 B |
1
assets/icons/debug_stop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square"><rect width="18" height="18" x="3" y="3" rx="2"/></svg>
|
||||
|
After Width: | Height: | Size: 266 B |
@@ -724,6 +724,14 @@
|
||||
"space": "project_panel::Open"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "VariableList",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"left": "variable_list::CollapseSelectedEntry",
|
||||
"right": "variable_list::ExpandSelectedEntry"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitPanel && ChangesList",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -1376,6 +1376,12 @@
|
||||
// }
|
||||
// ]
|
||||
"ssh_connections": [],
|
||||
|
||||
// Configures context servers for use in the Assistant.
|
||||
"context_servers": {}
|
||||
"context_servers": {},
|
||||
"debugger": {
|
||||
"stepping_granularity": "line",
|
||||
"save_breakpoints": true,
|
||||
"button": true
|
||||
}
|
||||
}
|
||||
|
||||
32
assets/settings/initial_debug_tasks.json
Normal file
@@ -0,0 +1,32 @@
|
||||
[
|
||||
{
|
||||
"label": "Debug active PHP file",
|
||||
"adapter": "php",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug active Python file",
|
||||
"adapter": "python",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "Debug active JavaScript file",
|
||||
"adapter": "javascript",
|
||||
"program": "$ZED_FILE",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT"
|
||||
},
|
||||
{
|
||||
"label": "JavaScript debug terminal",
|
||||
"adapter": "javascript",
|
||||
"request": "launch",
|
||||
"cwd": "$ZED_WORKTREE_ROOT",
|
||||
"initialize_args": {
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -20,7 +20,6 @@ extension_host.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
project.workspace = true
|
||||
smallvec.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
@@ -7,8 +7,7 @@ use gpui::{
|
||||
EventEmitter, InteractiveElement as _, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Transformation, Window,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerBinaryStatus, LanguageServerId};
|
||||
use lsp::LanguageServerName;
|
||||
use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
|
||||
use project::{EnvironmentErrorMessage, LanguageServerProgress, Project, WorktreeId};
|
||||
use smallvec::SmallVec;
|
||||
use std::{cmp::Reverse, fmt::Write, sync::Arc, time::Duration};
|
||||
@@ -20,21 +19,21 @@ actions!(activity_indicator, [ShowErrorMessage]);
|
||||
|
||||
pub enum Event {
|
||||
ShowError {
|
||||
lsp_name: LanguageServerName,
|
||||
server_name: SharedString,
|
||||
error: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct ActivityIndicator {
|
||||
statuses: Vec<LspStatus>,
|
||||
statuses: Vec<ServerStatus>,
|
||||
project: Entity<Project>,
|
||||
auto_updater: Option<Entity<AutoUpdater>>,
|
||||
context_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
struct LspStatus {
|
||||
name: LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
struct ServerStatus {
|
||||
name: SharedString,
|
||||
status: BinaryStatus,
|
||||
}
|
||||
|
||||
struct PendingWork<'a> {
|
||||
@@ -65,7 +64,20 @@ impl ActivityIndicator {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(&mut cx, |this: &mut ActivityIndicator, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(LspStatus { name, status });
|
||||
this.statuses.push(ServerStatus { name, status });
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut status_events = languages.dap_server_binary_statuses();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(ServerStatus { name, status });
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
@@ -88,18 +100,18 @@ impl ActivityIndicator {
|
||||
});
|
||||
|
||||
cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
|
||||
Event::ShowError { lsp_name, error } => {
|
||||
Event::ShowError { server_name, error } => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let project = project.clone();
|
||||
let error = error.clone();
|
||||
let lsp_name = lsp_name.clone();
|
||||
let server_name = server_name.clone();
|
||||
cx.spawn_in(window, |workspace, mut cx| async move {
|
||||
let buffer = create_buffer.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(
|
||||
0..0,
|
||||
format!("Language server error: {}\n\n{}", lsp_name, error),
|
||||
format!("Language server error: {}\n\n{}", server_name, error),
|
||||
)],
|
||||
None,
|
||||
cx,
|
||||
@@ -129,9 +141,9 @@ impl ActivityIndicator {
|
||||
|
||||
fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.statuses.retain(|status| {
|
||||
if let LanguageServerBinaryStatus::Failed { error } = &status.status {
|
||||
if let BinaryStatus::Failed { error } = &status.status {
|
||||
cx.emit(Event::ShowError {
|
||||
lsp_name: status.name.clone(),
|
||||
server_name: status.name.clone(),
|
||||
error: error.clone(),
|
||||
});
|
||||
false
|
||||
@@ -260,12 +272,10 @@ impl ActivityIndicator {
|
||||
let mut failed = SmallVec::<[_; 3]>::new();
|
||||
for status in &self.statuses {
|
||||
match status.status {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => {
|
||||
checking_for_update.push(status.name.clone())
|
||||
}
|
||||
LanguageServerBinaryStatus::Downloading => downloading.push(status.name.clone()),
|
||||
LanguageServerBinaryStatus::Failed { .. } => failed.push(status.name.clone()),
|
||||
LanguageServerBinaryStatus::None => {}
|
||||
BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
|
||||
BinaryStatus::Downloading => downloading.push(status.name.clone()),
|
||||
BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
|
||||
BinaryStatus::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +288,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: format!(
|
||||
"Downloading {}...",
|
||||
downloading.iter().map(|name| name.0.as_ref()).fold(
|
||||
downloading.iter().map(|name| name.as_ref()).fold(
|
||||
String::new(),
|
||||
|mut acc, s| {
|
||||
if !acc.is_empty() {
|
||||
@@ -306,7 +316,7 @@ impl ActivityIndicator {
|
||||
),
|
||||
message: format!(
|
||||
"Checking for updates to {}...",
|
||||
checking_for_update.iter().map(|name| name.0.as_ref()).fold(
|
||||
checking_for_update.iter().map(|name| name.as_ref()).fold(
|
||||
String::new(),
|
||||
|mut acc, s| {
|
||||
if !acc.is_empty() {
|
||||
@@ -336,7 +346,7 @@ impl ActivityIndicator {
|
||||
"Failed to run {}. Click to show error.",
|
||||
failed
|
||||
.iter()
|
||||
.map(|name| name.0.as_ref())
|
||||
.map(|name| name.as_ref())
|
||||
.fold(String::new(), |mut acc, s| {
|
||||
if !acc.is_empty() {
|
||||
acc.push_str(", ");
|
||||
|
||||
@@ -89,8 +89,11 @@ channel.workspace = true
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
collab_ui = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
command_palette_hooks.workspace = true
|
||||
context_server.workspace = true
|
||||
ctor.workspace = true
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
debugger_ui = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
extension.workspace = true
|
||||
|
||||
@@ -469,3 +469,14 @@ CREATE TABLE IF NOT EXISTS processed_stripe_events (
|
||||
);
|
||||
|
||||
CREATE INDEX "ix_processed_stripe_events_on_stripe_event_created_timestamp" ON processed_stripe_events (stripe_event_created_timestamp);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "breakpoints" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"position" INTEGER NOT NULL,
|
||||
"log_message" TEXT NULL,
|
||||
"worktree_id" BIGINT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"kind" VARCHAR NOT NULL
|
||||
);
|
||||
CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");
|
||||
|
||||
11
crates/collab/migrations/20241121185750_add_breakpoints.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
CREATE TABLE IF NOT EXISTS "breakpoints" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"project_id" INTEGER NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
|
||||
"position" INTEGER NOT NULL,
|
||||
"log_message" TEXT NULL,
|
||||
"worktree_id" BIGINT NOT NULL,
|
||||
"path" TEXT NOT NULL,
|
||||
"kind" VARCHAR NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX "index_breakpoints_on_project_id" ON "breakpoints" ("project_id");
|
||||
@@ -659,6 +659,7 @@ pub struct RejoinedProject {
|
||||
pub collaborators: Vec<ProjectCollaborator>,
|
||||
pub worktrees: Vec<RejoinedWorktree>,
|
||||
pub language_servers: Vec<proto::LanguageServer>,
|
||||
pub breakpoints: HashMap<proto::ProjectPath, HashSet<proto::Breakpoint>>,
|
||||
}
|
||||
|
||||
impl RejoinedProject {
|
||||
@@ -681,6 +682,17 @@ impl RejoinedProject {
|
||||
.map(|collaborator| collaborator.to_proto())
|
||||
.collect(),
|
||||
language_servers: self.language_servers.clone(),
|
||||
breakpoints: self
|
||||
.breakpoints
|
||||
.iter()
|
||||
.map(
|
||||
|(project_path, breakpoints)| proto::SynchronizeBreakpoints {
|
||||
project_id: self.id.to_proto(),
|
||||
breakpoints: breakpoints.iter().cloned().collect(),
|
||||
project_path: Some(project_path.clone()),
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,6 +739,7 @@ pub struct Project {
|
||||
pub collaborators: Vec<ProjectCollaborator>,
|
||||
pub worktrees: BTreeMap<u64, Worktree>,
|
||||
pub language_servers: Vec<proto::LanguageServer>,
|
||||
pub breakpoints: HashMap<proto::ProjectPath, HashSet<proto::Breakpoint>>,
|
||||
}
|
||||
|
||||
pub struct ProjectCollaborator {
|
||||
|
||||
@@ -94,6 +94,9 @@ id_type!(RoomParticipantId);
|
||||
id_type!(ServerId);
|
||||
id_type!(SignupId);
|
||||
id_type!(UserId);
|
||||
id_type!(DebugClientId);
|
||||
id_type!(SessionId);
|
||||
id_type!(ThreadId);
|
||||
|
||||
/// ChannelRole gives you permissions for both channels and calls.
|
||||
#[derive(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Context as _;
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use util::ResultExt;
|
||||
|
||||
use super::*;
|
||||
@@ -571,6 +571,60 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_breakpoints(
|
||||
&self,
|
||||
connection_id: ConnectionId,
|
||||
update: &proto::SynchronizeBreakpoints,
|
||||
) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
|
||||
let project_id = ProjectId::from_proto(update.project_id);
|
||||
self.project_transaction(project_id, |tx| async move {
|
||||
let project_path = update
|
||||
.project_path
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("invalid project path"))?;
|
||||
|
||||
// Ensure the update comes from the host.
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
|
||||
// remove all existing breakpoints
|
||||
breakpoints::Entity::delete_many()
|
||||
.filter(breakpoints::Column::ProjectId.eq(project.id))
|
||||
.exec(&*tx)
|
||||
.await?;
|
||||
|
||||
if !update.breakpoints.is_empty() {
|
||||
breakpoints::Entity::insert_many(update.breakpoints.iter().map(|breakpoint| {
|
||||
breakpoints::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
project_id: ActiveValue::Set(project_id),
|
||||
worktree_id: ActiveValue::Set(project_path.worktree_id as i64),
|
||||
path: ActiveValue::Set(project_path.path.clone()),
|
||||
kind: match proto::BreakpointKind::from_i32(breakpoint.kind) {
|
||||
Some(proto::BreakpointKind::Log) => {
|
||||
ActiveValue::Set(breakpoints::BreakpointKind::Log)
|
||||
}
|
||||
Some(proto::BreakpointKind::Standard) => {
|
||||
ActiveValue::Set(breakpoints::BreakpointKind::Standard)
|
||||
}
|
||||
None => ActiveValue::Set(breakpoints::BreakpointKind::Standard),
|
||||
},
|
||||
log_message: ActiveValue::Set(breakpoint.message.clone()),
|
||||
position: ActiveValue::Set(breakpoint.cached_position as i32),
|
||||
}
|
||||
}))
|
||||
.exec_without_returning(&*tx)
|
||||
.await?;
|
||||
}
|
||||
|
||||
self.internal_project_connection_ids(project_id, connection_id, true, &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Updates the worktree settings for the given connection.
|
||||
pub async fn update_worktree_settings(
|
||||
&self,
|
||||
@@ -852,6 +906,33 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
let mut breakpoints: HashMap<proto::ProjectPath, HashSet<proto::Breakpoint>> =
|
||||
HashMap::default();
|
||||
|
||||
let db_breakpoints = project.find_related(breakpoints::Entity).all(tx).await?;
|
||||
|
||||
for breakpoint in db_breakpoints.iter() {
|
||||
let project_path = proto::ProjectPath {
|
||||
worktree_id: breakpoint.worktree_id as u64,
|
||||
path: breakpoint.path.clone(),
|
||||
};
|
||||
|
||||
breakpoints
|
||||
.entry(project_path)
|
||||
.or_default()
|
||||
.insert(proto::Breakpoint {
|
||||
position: None,
|
||||
cached_position: breakpoint.position as u32,
|
||||
kind: match breakpoint.kind {
|
||||
breakpoints::BreakpointKind::Standard => {
|
||||
proto::BreakpointKind::Standard.into()
|
||||
}
|
||||
breakpoints::BreakpointKind::Log => proto::BreakpointKind::Log.into(),
|
||||
},
|
||||
message: breakpoint.log_message.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Populate language servers.
|
||||
let language_servers = project
|
||||
.find_related(language_server::Entity)
|
||||
@@ -879,6 +960,7 @@ impl Database {
|
||||
worktree_id: None,
|
||||
})
|
||||
.collect(),
|
||||
breakpoints,
|
||||
};
|
||||
Ok((project, replica_id as ReplicaId))
|
||||
}
|
||||
@@ -1106,41 +1188,52 @@ impl Database {
|
||||
exclude_dev_server: bool,
|
||||
) -> Result<TransactionGuard<HashSet<ConnectionId>>> {
|
||||
self.project_transaction(project_id, |tx| async move {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
|
||||
let mut collaborators = project_collaborator::Entity::find()
|
||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
|
||||
let mut connection_ids = HashSet::default();
|
||||
if let Some(host_connection) = project.host_connection().log_err() {
|
||||
if !exclude_dev_server {
|
||||
connection_ids.insert(host_connection);
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(collaborator) = collaborators.next().await {
|
||||
let collaborator = collaborator?;
|
||||
connection_ids.insert(collaborator.connection());
|
||||
}
|
||||
|
||||
if connection_ids.contains(&connection_id)
|
||||
|| Some(connection_id) == project.host_connection().ok()
|
||||
{
|
||||
Ok(connection_ids)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"can only send project updates to a project you're in"
|
||||
))?
|
||||
}
|
||||
self.internal_project_connection_ids(project_id, connection_id, exclude_dev_server, &tx)
|
||||
.await
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn internal_project_connection_ids(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
exclude_dev_server: bool,
|
||||
tx: &DatabaseTransaction,
|
||||
) -> Result<HashSet<ConnectionId>> {
|
||||
let project = project::Entity::find_by_id(project_id)
|
||||
.one(tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
|
||||
let mut collaborators = project_collaborator::Entity::find()
|
||||
.filter(project_collaborator::Column::ProjectId.eq(project_id))
|
||||
.stream(tx)
|
||||
.await?;
|
||||
|
||||
let mut connection_ids = HashSet::default();
|
||||
if let Some(host_connection) = project.host_connection().log_err() {
|
||||
if !exclude_dev_server {
|
||||
connection_ids.insert(host_connection);
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(collaborator) = collaborators.next().await {
|
||||
let collaborator = collaborator?;
|
||||
connection_ids.insert(collaborator.connection());
|
||||
}
|
||||
|
||||
if connection_ids.contains(&connection_id)
|
||||
|| Some(connection_id) == project.host_connection().ok()
|
||||
{
|
||||
Ok(connection_ids)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"can only send project updates to a project you're in"
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
async fn project_guest_connection_ids(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
|
||||
@@ -765,6 +765,33 @@ impl Database {
|
||||
worktrees.push(worktree);
|
||||
}
|
||||
|
||||
let mut breakpoints: HashMap<proto::ProjectPath, HashSet<proto::Breakpoint>> =
|
||||
HashMap::default();
|
||||
|
||||
let db_breakpoints = project.find_related(breakpoints::Entity).all(tx).await?;
|
||||
|
||||
for breakpoint in db_breakpoints.iter() {
|
||||
let project_path = proto::ProjectPath {
|
||||
worktree_id: breakpoint.worktree_id as u64,
|
||||
path: breakpoint.path.clone(),
|
||||
};
|
||||
|
||||
breakpoints
|
||||
.entry(project_path)
|
||||
.or_default()
|
||||
.insert(proto::Breakpoint {
|
||||
position: None,
|
||||
cached_position: breakpoint.position as u32,
|
||||
kind: match breakpoint.kind {
|
||||
breakpoints::BreakpointKind::Standard => {
|
||||
proto::BreakpointKind::Standard.into()
|
||||
}
|
||||
breakpoints::BreakpointKind::Log => proto::BreakpointKind::Log.into(),
|
||||
},
|
||||
message: breakpoint.log_message.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let language_servers = project
|
||||
.find_related(language_server::Entity)
|
||||
.all(tx)
|
||||
@@ -834,6 +861,7 @@ impl Database {
|
||||
collaborators,
|
||||
worktrees,
|
||||
language_servers,
|
||||
breakpoints,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ pub mod access_token;
|
||||
pub mod billing_customer;
|
||||
pub mod billing_preference;
|
||||
pub mod billing_subscription;
|
||||
pub mod breakpoints;
|
||||
pub mod buffer;
|
||||
pub mod buffer_operation;
|
||||
pub mod buffer_snapshot;
|
||||
|
||||
47
crates/collab/src/db/tables/breakpoints.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use crate::db::ProjectId;
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "breakpoints")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
#[sea_orm(primary_key)]
|
||||
pub project_id: ProjectId,
|
||||
pub worktree_id: i64,
|
||||
pub path: String,
|
||||
pub kind: BreakpointKind,
|
||||
pub log_message: Option<String>,
|
||||
pub position: i32,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum, Default, Hash, serde::Serialize,
|
||||
)]
|
||||
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BreakpointKind {
|
||||
#[default]
|
||||
#[sea_orm(string_value = "standard")]
|
||||
Standard,
|
||||
#[sea_orm(string_value = "log")]
|
||||
Log,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::project::Entity",
|
||||
from = "Column::ProjectId",
|
||||
to = "super::project::Column::Id"
|
||||
)]
|
||||
Project,
|
||||
}
|
||||
|
||||
impl Related<super::project::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Project.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -49,6 +49,8 @@ pub enum Relation {
|
||||
Collaborators,
|
||||
#[sea_orm(has_many = "super::language_server::Entity")]
|
||||
LanguageServers,
|
||||
#[sea_orm(has_many = "super::breakpoints::Entity")]
|
||||
Breakpoints,
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
@@ -81,4 +83,10 @@ impl Related<super::language_server::Entity> for Entity {
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::breakpoints::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Breakpoints.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
@@ -427,7 +427,40 @@ impl Server {
|
||||
app_state.config.openai_api_key.clone(),
|
||||
)
|
||||
}
|
||||
});
|
||||
})
|
||||
.add_message_handler(update_breakpoints)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::SetActiveDebugLine>)
|
||||
.add_message_handler(
|
||||
broadcast_project_message_from_host::<proto::RemoveActiveDebugLine>,
|
||||
)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDebugAdapter>)
|
||||
.add_message_handler(
|
||||
broadcast_project_message_from_host::<proto::SetDebugClientCapabilities>,
|
||||
)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::ShutdownDebugClient>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapNextRequest>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapStepInRequest>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapStepOutRequest>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapStepBackRequest>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapContinueRequest>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapPauseRequest>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapDisconnectRequest>)
|
||||
.add_request_handler(
|
||||
forward_mutating_project_request::<proto::DapTerminateThreadsRequest>,
|
||||
)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapRestartRequest>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::DapTerminateRequest>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateThreadStatus>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::VariablesRequest>)
|
||||
.add_message_handler(
|
||||
broadcast_project_message_from_host::<proto::DapRestartStackFrameRequest>,
|
||||
)
|
||||
.add_message_handler(
|
||||
broadcast_project_message_from_host::<proto::ToggleIgnoreBreakpoints>,
|
||||
)
|
||||
.add_message_handler(
|
||||
broadcast_project_message_from_host::<proto::IgnoreBreakpointState>,
|
||||
);
|
||||
|
||||
Arc::new(server)
|
||||
}
|
||||
@@ -1867,6 +1900,18 @@ fn join_project_internal(
|
||||
.trace_err();
|
||||
}
|
||||
|
||||
let breakpoints = project
|
||||
.breakpoints
|
||||
.iter()
|
||||
.map(
|
||||
|(project_path, breakpoint_set)| proto::SynchronizeBreakpoints {
|
||||
project_id: project.id.0 as u64,
|
||||
breakpoints: breakpoint_set.iter().map(|bp| bp.clone()).collect(),
|
||||
project_path: Some(project_path.clone()),
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
|
||||
// First, we send the metadata associated with each worktree.
|
||||
response.send(proto::JoinProjectResponse {
|
||||
project_id: project.id.0 as u64,
|
||||
@@ -1875,6 +1920,7 @@ fn join_project_internal(
|
||||
collaborators: collaborators.clone(),
|
||||
language_servers: project.language_servers.clone(),
|
||||
role: project.role.into(),
|
||||
breakpoints,
|
||||
})?;
|
||||
|
||||
for (worktree_id, worktree) in mem::take(&mut project.worktrees) {
|
||||
@@ -2061,7 +2107,7 @@ async fn update_worktree_settings(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that a language server has started.
|
||||
/// Notify other participants that a language server has started.
|
||||
async fn start_language_server(
|
||||
request: proto::StartLanguageServer,
|
||||
session: Session,
|
||||
@@ -2107,6 +2153,29 @@ async fn update_language_server(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that breakpoints have changed.
|
||||
async fn update_breakpoints(
|
||||
request: proto::SynchronizeBreakpoints,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let guest_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
.update_breakpoints(session.connection_id, &request)
|
||||
.await?;
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
guest_connection_ids.iter().copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, request.clone())
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// forward a project request to the host. These requests should be read only
|
||||
/// as guests are allowed to send them.
|
||||
async fn forward_read_only_project_request<T>(
|
||||
|
||||
@@ -11,6 +11,7 @@ mod channel_buffer_tests;
|
||||
mod channel_guest_tests;
|
||||
mod channel_message_tests;
|
||||
mod channel_tests;
|
||||
mod debug_panel_tests;
|
||||
mod editor_tests;
|
||||
mod following_tests;
|
||||
mod integration_tests;
|
||||
|
||||
2444
crates/collab/src/tests/debug_panel_tests.rs
Normal file
@@ -22,7 +22,7 @@ use language::{
|
||||
};
|
||||
use project::{
|
||||
project_settings::{InlineBlameSettings, ProjectSettings},
|
||||
SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
};
|
||||
use recent_projects::disconnected_overlay::DisconnectedOverlay;
|
||||
use rpc::RECEIVE_TIMEOUT;
|
||||
@@ -1591,6 +1591,8 @@ async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
|
||||
// Client B joins the project
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
@@ -2394,6 +2396,202 @@ fn main() { let foo = other::foo(); }"};
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
let executor = cx_a.executor();
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
client_a
|
||||
.fs()
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"test.txt": "one\ntwo\nthree\nfour\nfive",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: Arc::from(Path::new(&"test.txt")),
|
||||
};
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.join_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
|
||||
let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
|
||||
|
||||
// Client A opens an editor.
|
||||
let editor_a = workspace_a
|
||||
.update_in(cx_a, |workspace, window, cx| {
|
||||
workspace.open_path(project_path.clone(), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
// Client B opens same editor as A.
|
||||
let editor_b = workspace_b
|
||||
.update_in(cx_b, |workspace, window, cx| {
|
||||
workspace.open_path(project_path.clone(), None, true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
// Client A adds breakpoint on line (1)
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(1, breakpoints_a.len());
|
||||
assert_eq!(1, breakpoints_a.get(&project_path).unwrap().len());
|
||||
assert_eq!(breakpoints_a, breakpoints_b);
|
||||
|
||||
// Client B adds breakpoint on line(2)
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.move_down(&editor::actions::MoveDown, window, cx);
|
||||
editor.move_down(&editor::actions::MoveDown, window, cx);
|
||||
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(1, breakpoints_a.len());
|
||||
assert_eq!(breakpoints_a, breakpoints_b);
|
||||
assert_eq!(2, breakpoints_a.get(&project_path).unwrap().len());
|
||||
|
||||
// Client A removes last added breakpoint from client B
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
editor.move_down(&editor::actions::MoveDown, window, cx);
|
||||
editor.move_down(&editor::actions::MoveDown, window, cx);
|
||||
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(1, breakpoints_a.len());
|
||||
assert_eq!(breakpoints_a, breakpoints_b);
|
||||
assert_eq!(1, breakpoints_a.get(&project_path).unwrap().len());
|
||||
|
||||
// Client B removes first added breakpoint by client A
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
editor.move_up(&editor::actions::MoveUp, window, cx);
|
||||
editor.move_up(&editor::actions::MoveUp, window, cx);
|
||||
editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
|
||||
let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
|
||||
editor
|
||||
.breakpoint_store()
|
||||
.clone()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(0, breakpoints_a.len());
|
||||
assert_eq!(breakpoints_a, breakpoints_b);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn tab_undo_assert(
|
||||
cx_a: &mut EditorTestContext,
|
||||
|
||||
54
crates/dap/Cargo.toml
Normal file
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "dap"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
"util/test-support",
|
||||
"task/test-support",
|
||||
"async-pipe",
|
||||
"settings/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-compression.workspace = true
|
||||
async-pipe = { workspace = true, optional = true }
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "bf5632dc19f806e8a435c9f04a4bfe7322badea2" }
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
node_runtime.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
sysinfo.workspace = true
|
||||
task.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
async-pipe.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
task = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
1
crates/dap/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
9
crates/dap/docs/breakpoints.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Overview
|
||||
|
||||
The active `Project` is responsible for maintain opened and closed breakpoints
|
||||
as well as serializing breakpoints to save. At a high level project serializes
|
||||
the positions of breakpoints that don't belong to any active buffers and handles
|
||||
converting breakpoints from serializing to active whenever a buffer is opened/closed.
|
||||
|
||||
`Project` also handles sending all relevant breakpoint information to debug adapter's
|
||||
during debugging or when starting a debugger.
|
||||
391
crates/dap/src/adapters.rs
Normal file
@@ -0,0 +1,391 @@
|
||||
use ::fs::Fs;
|
||||
use anyhow::{anyhow, Context as _, Ok, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use futures::io::BufReader;
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
pub use http_client::{github::latest_github_release, HttpClient};
|
||||
use language::LanguageToolchainStore;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use settings::WorktreeId;
|
||||
use smol::{self, fs::File, lock::Mutex};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
ffi::{OsStr, OsString},
|
||||
fmt::Debug,
|
||||
net::Ipv4Addr,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use sysinfo::{Pid, Process};
|
||||
use task::DebugAdapterConfig;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DapStatus {
|
||||
None,
|
||||
CheckingForUpdate,
|
||||
Downloading,
|
||||
Failed { error: String },
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait DapDelegate {
|
||||
fn worktree_id(&self) -> WorktreeId;
|
||||
fn http_client(&self) -> Arc<dyn HttpClient>;
|
||||
fn node_runtime(&self) -> NodeRuntime;
|
||||
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
|
||||
fn fs(&self) -> Arc<dyn Fs>;
|
||||
fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
|
||||
fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
|
||||
fn which(&self, command: &OsStr) -> Option<PathBuf>;
|
||||
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
||||
pub struct DebugAdapterName(pub Arc<str>);
|
||||
|
||||
impl Deref for DebugAdapterName {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for DebugAdapterName {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for DebugAdapterName {
|
||||
fn as_ref(&self) -> &Path {
|
||||
Path::new(&*self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DebugAdapterName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DebugAdapterName> for SharedString {
|
||||
fn from(name: DebugAdapterName) -> Self {
|
||||
SharedString::from(name.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for DebugAdapterName {
|
||||
fn from(str: &'a str) -> DebugAdapterName {
|
||||
DebugAdapterName(str.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TcpArguments {
|
||||
pub host: Ipv4Addr,
|
||||
pub port: u16,
|
||||
pub timeout: Option<u64>,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DebugAdapterBinary {
|
||||
pub command: String,
|
||||
pub arguments: Option<Vec<OsString>>,
|
||||
pub envs: Option<HashMap<String, String>>,
|
||||
pub cwd: Option<PathBuf>,
|
||||
pub connection: Option<TcpArguments>,
|
||||
}
|
||||
|
||||
pub struct AdapterVersion {
|
||||
pub tag_name: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
pub enum DownloadedFileType {
|
||||
Vsix,
|
||||
GzipTar,
|
||||
Zip,
|
||||
}
|
||||
|
||||
pub struct GithubRepo {
|
||||
pub repo_name: String,
|
||||
pub repo_owner: String,
|
||||
}
|
||||
|
||||
pub async fn download_adapter_from_github(
|
||||
adapter_name: DebugAdapterName,
|
||||
github_version: AdapterVersion,
|
||||
file_type: DownloadedFileType,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<PathBuf> {
|
||||
let adapter_path = paths::debug_adapters_dir().join(&adapter_name);
|
||||
let version_path = adapter_path.join(format!("{}_{}", adapter_name, github_version.tag_name));
|
||||
let fs = delegate.fs();
|
||||
|
||||
if version_path.exists() {
|
||||
return Ok(version_path);
|
||||
}
|
||||
|
||||
if !adapter_path.exists() {
|
||||
fs.create_dir(&adapter_path.as_path())
|
||||
.await
|
||||
.context("Failed creating adapter path")?;
|
||||
}
|
||||
|
||||
log::debug!(
|
||||
"Downloading adapter {} from {}",
|
||||
adapter_name,
|
||||
&github_version.url,
|
||||
);
|
||||
|
||||
let mut response = delegate
|
||||
.http_client()
|
||||
.get(&github_version.url, Default::default(), true)
|
||||
.await
|
||||
.context("Error downloading release")?;
|
||||
if !response.status().is_success() {
|
||||
Err(anyhow!(
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
))?;
|
||||
}
|
||||
|
||||
match file_type {
|
||||
DownloadedFileType::GzipTar => {
|
||||
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(&version_path).await?;
|
||||
}
|
||||
DownloadedFileType::Zip | DownloadedFileType::Vsix => {
|
||||
let zip_path = version_path.with_extension("zip");
|
||||
|
||||
let mut file = File::create(&zip_path).await?;
|
||||
futures::io::copy(response.body_mut(), &mut file).await?;
|
||||
|
||||
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
|
||||
util::command::new_smol_command("unzip")
|
||||
.arg(&zip_path)
|
||||
.arg("-d")
|
||||
.arg(&version_path)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
util::fs::remove_matching(&adapter_path, |entry| {
|
||||
entry
|
||||
.file_name()
|
||||
.is_some_and(|file| file.to_string_lossy().ends_with(".zip"))
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// remove older versions
|
||||
util::fs::remove_matching(&adapter_path, |entry| {
|
||||
entry.to_string_lossy() != version_path.to_string_lossy()
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(version_path)
|
||||
}
|
||||
|
||||
pub async fn fetch_latest_adapter_version_from_github(
|
||||
github_repo: GithubRepo,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion> {
|
||||
let release = latest_github_release(
|
||||
&format!("{}/{}", github_repo.repo_owner, github_repo.repo_name),
|
||||
false,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(AdapterVersion {
|
||||
tag_name: release.tag_name,
|
||||
url: release.zipball_url,
|
||||
})
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait DebugAdapter: 'static + Send + Sync {
|
||||
fn name(&self) -> DebugAdapterName;
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
if delegate
|
||||
.updated_adapters()
|
||||
.lock()
|
||||
.await
|
||||
.contains(&self.name())
|
||||
{
|
||||
log::info!("Using cached debug adapter binary {}", self.name());
|
||||
|
||||
if let Some(binary) = self
|
||||
.get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
return Ok(binary);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Cached binary {} is corrupt falling back to install",
|
||||
self.name()
|
||||
);
|
||||
}
|
||||
|
||||
log::info!("Getting latest version of debug adapter {}", self.name());
|
||||
delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
|
||||
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
|
||||
log::info!(
|
||||
"Installiing latest version of debug adapter {}",
|
||||
self.name()
|
||||
);
|
||||
delegate.update_status(self.name(), DapStatus::Downloading);
|
||||
self.install_binary(version, delegate).await?;
|
||||
|
||||
delegate
|
||||
.updated_adapters()
|
||||
.lock_arc()
|
||||
.await
|
||||
.insert(self.name());
|
||||
}
|
||||
|
||||
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion>;
|
||||
|
||||
/// Installs the binary for the debug adapter.
|
||||
/// This method is called when the adapter binary is not found or needs to be updated.
|
||||
/// It should download and install the necessary files for the debug adapter to function.
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary>;
|
||||
|
||||
/// Should return base configuration to make the debug adapter work
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value;
|
||||
|
||||
/// Filters out the processes that the adapter can attach to for debugging
|
||||
fn attach_processes<'a>(
|
||||
&self,
|
||||
_: &'a HashMap<Pid, Process>,
|
||||
) -> Option<Vec<(&'a Pid, &'a Process)>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeAdapter {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeAdapter {
|
||||
const ADAPTER_NAME: &'static str = "fake-adapter";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for FakeAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
_: &DebugAdapterConfig,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
Ok(DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
arguments: None,
|
||||
connection: None,
|
||||
envs: None,
|
||||
cwd: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion> {
|
||||
unimplemented!("fetch latest adapter version");
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
_version: AdapterVersion,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
unimplemented!("install binary");
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
_: &DebugAdapterConfig,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
unimplemented!("get installed binary");
|
||||
}
|
||||
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
||||
use serde_json::json;
|
||||
use task::DebugRequestType;
|
||||
|
||||
json!({
|
||||
"request": match config.request {
|
||||
DebugRequestType::Launch => "launch",
|
||||
DebugRequestType::Attach(_) => "attach",
|
||||
},
|
||||
"process_id": if let DebugRequestType::Attach(attach_config) = &config.request {
|
||||
attach_config.process_id
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn attach_processes<'a>(
|
||||
&self,
|
||||
processes: &'a HashMap<Pid, Process>,
|
||||
) -> Option<Vec<(&'a Pid, &'a Process)>> {
|
||||
Some(
|
||||
processes
|
||||
.iter()
|
||||
.filter(|(pid, _)| pid.as_u32() == std::process::id())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
485
crates/dap/src/client.rs
Normal file
@@ -0,0 +1,485 @@
|
||||
use crate::{
|
||||
adapters::DebugAdapterBinary,
|
||||
transport::{IoKind, LogKind, TransportDelegate},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use dap_types::{
|
||||
messages::{Message, Response},
|
||||
requests::Request,
|
||||
};
|
||||
use futures::{channel::oneshot, select, FutureExt as _};
|
||||
use gpui::{App, AsyncApp, BackgroundExecutor};
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use std::{
|
||||
hash::Hash,
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
const DAP_REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[repr(transparent)]
|
||||
pub struct SessionId(pub u32);
|
||||
|
||||
impl SessionId {
|
||||
pub fn from_proto(client_id: u64) -> Self {
|
||||
Self(client_id as u32)
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> u64 {
|
||||
self.0 as u64
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a connection to the debug adapter process, either via stdout/stdin or a socket.
|
||||
pub struct DebugAdapterClient {
|
||||
id: SessionId,
|
||||
sequence_count: AtomicU64,
|
||||
binary: DebugAdapterBinary,
|
||||
executor: BackgroundExecutor,
|
||||
transport_delegate: TransportDelegate,
|
||||
}
|
||||
|
||||
pub type DapMessageHandler = Box<dyn FnMut(Message, &mut App) + 'static + Send + Sync>;
|
||||
|
||||
impl DebugAdapterClient {
|
||||
pub async fn start(
|
||||
id: SessionId,
|
||||
binary: DebugAdapterBinary,
|
||||
message_handler: DapMessageHandler,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let ((server_rx, server_tx), transport_delegate) =
|
||||
TransportDelegate::start(&binary, cx.clone()).await?;
|
||||
let this = Self {
|
||||
id,
|
||||
binary,
|
||||
transport_delegate,
|
||||
sequence_count: AtomicU64::new(1),
|
||||
executor: cx.background_executor().clone(),
|
||||
};
|
||||
log::info!("Successfully connected to debug adapter");
|
||||
|
||||
let client_id = this.id;
|
||||
|
||||
// start handling events/reverse requests
|
||||
cx.update(|cx| {
|
||||
cx.spawn({
|
||||
let server_tx = server_tx.clone();
|
||||
|mut cx| async move {
|
||||
Self::handle_receive_messages(
|
||||
client_id,
|
||||
server_rx,
|
||||
server_tx,
|
||||
message_handler,
|
||||
&mut cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn reconnect(
|
||||
&self,
|
||||
session_id: SessionId,
|
||||
binary: DebugAdapterBinary,
|
||||
message_handler: DapMessageHandler,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let binary = match self.transport_delegate.transport() {
|
||||
crate::transport::Transport::Tcp(tcp_transport) => DebugAdapterBinary {
|
||||
command: binary.command,
|
||||
arguments: binary.arguments,
|
||||
envs: binary.envs,
|
||||
cwd: binary.cwd,
|
||||
connection: Some(crate::adapters::TcpArguments {
|
||||
host: tcp_transport.host,
|
||||
port: tcp_transport.port,
|
||||
timeout: Some(tcp_transport.timeout),
|
||||
}),
|
||||
},
|
||||
_ => self.binary.clone(),
|
||||
};
|
||||
|
||||
Self::start(session_id, binary, message_handler, cx).await
|
||||
}
|
||||
|
||||
async fn handle_receive_messages(
|
||||
client_id: SessionId,
|
||||
server_rx: Receiver<Message>,
|
||||
client_tx: Sender<Message>,
|
||||
mut message_handler: DapMessageHandler,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let result = loop {
|
||||
let message = match server_rx.recv().await {
|
||||
Ok(message) => message,
|
||||
Err(e) => break Err(e.into()),
|
||||
};
|
||||
|
||||
if let Err(e) = match message {
|
||||
Message::Event(ev) => {
|
||||
log::debug!("Client {} received event `{}`", client_id.0, &ev);
|
||||
|
||||
cx.update(|cx| message_handler(Message::Event(ev), cx))
|
||||
}
|
||||
Message::Request(req) => cx.update(|cx| message_handler(Message::Request(req), cx)),
|
||||
Message::Response(response) => {
|
||||
log::debug!("Received response after request timeout: {:#?}", response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
} {
|
||||
break Err(e);
|
||||
}
|
||||
|
||||
smol::future::yield_now().await;
|
||||
};
|
||||
|
||||
drop(client_tx);
|
||||
|
||||
log::debug!("Handle receive messages dropped");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Send a request to an adapter and get a response back
|
||||
/// Note: This function will block until a response is sent back from the adapter
|
||||
pub async fn request<R: Request>(&self, arguments: R::Arguments) -> Result<R::Response> {
|
||||
let serialized_arguments = serde_json::to_value(arguments)?;
|
||||
|
||||
let (callback_tx, callback_rx) = oneshot::channel::<Result<Response>>();
|
||||
|
||||
let sequence_id = self.next_sequence_id();
|
||||
|
||||
let request = crate::messages::Request {
|
||||
seq: sequence_id,
|
||||
command: R::COMMAND.to_string(),
|
||||
arguments: Some(serialized_arguments),
|
||||
};
|
||||
|
||||
self.transport_delegate
|
||||
.add_pending_request(sequence_id, callback_tx)
|
||||
.await;
|
||||
|
||||
log::debug!(
|
||||
"Client {} send `{}` request with sequence_id: {}",
|
||||
self.id.0,
|
||||
R::COMMAND.to_string(),
|
||||
sequence_id
|
||||
);
|
||||
|
||||
self.send_message(Message::Request(request)).await?;
|
||||
|
||||
let mut timeout = self.executor.timer(DAP_REQUEST_TIMEOUT).fuse();
|
||||
let command = R::COMMAND.to_string();
|
||||
|
||||
select! {
|
||||
response = callback_rx.fuse() => {
|
||||
log::debug!(
|
||||
"Client {} received response for: `{}` sequence_id: {}",
|
||||
self.id.0,
|
||||
command,
|
||||
sequence_id
|
||||
);
|
||||
|
||||
let response = response??;
|
||||
match response.success {
|
||||
true => Ok(serde_json::from_value(response.body.unwrap_or_default())?),
|
||||
false => Err(anyhow!("Request failed")),
|
||||
}
|
||||
}
|
||||
|
||||
_ = timeout => {
|
||||
self.transport_delegate.cancel_pending_request(&sequence_id).await;
|
||||
log::error!("Cancelled DAP request for {command:?} id {sequence_id} which took over {DAP_REQUEST_TIMEOUT:?}");
|
||||
anyhow::bail!("DAP request timeout");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_message(&self, message: Message) -> Result<()> {
|
||||
self.transport_delegate.send_message(message).await
|
||||
}
|
||||
|
||||
pub fn id(&self) -> SessionId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn binary(&self) -> &DebugAdapterBinary {
|
||||
&self.binary
|
||||
}
|
||||
|
||||
/// Get the next sequence id to be used in a request
|
||||
pub fn next_sequence_id(&self) -> u64 {
|
||||
self.sequence_count.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
self.transport_delegate.shutdown().await
|
||||
}
|
||||
|
||||
pub fn has_adapter_logs(&self) -> bool {
|
||||
self.transport_delegate.has_adapter_logs()
|
||||
}
|
||||
|
||||
pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
|
||||
where
|
||||
F: 'static + Send + FnMut(IoKind, &str),
|
||||
{
|
||||
self.transport_delegate.add_log_handler(f, kind);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn on_request<R: dap_types::requests::Request, F>(&self, handler: F)
|
||||
where
|
||||
F: 'static
|
||||
+ Send
|
||||
+ FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>,
|
||||
{
|
||||
let transport = self.transport_delegate.transport().as_fake();
|
||||
transport.on_request::<R, F>(handler).await;
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn fake_reverse_request<R: dap_types::requests::Request>(&self, args: R::Arguments) {
|
||||
self.send_message(Message::Request(dap_types::messages::Request {
|
||||
seq: self.sequence_count.load(Ordering::Relaxed),
|
||||
command: R::COMMAND.into(),
|
||||
arguments: serde_json::to_value(args).ok(),
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn on_response<R: dap_types::requests::Request, F>(&self, handler: F)
|
||||
where
|
||||
F: 'static + Send + Fn(Response),
|
||||
{
|
||||
let transport = self.transport_delegate.transport().as_fake();
|
||||
transport.on_response::<R, F>(handler).await;
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub async fn fake_event(&self, event: dap_types::messages::Events) {
|
||||
self.send_message(Message::Event(Box::new(event)))
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{client::DebugAdapterClient, debugger_settings::DebuggerSettings};
|
||||
use dap_types::{
|
||||
messages::Events,
|
||||
requests::{Initialize, Request, RunInTerminal},
|
||||
Capabilities, InitializeRequestArguments, InitializeRequestArgumentsPathFormat,
|
||||
RunInTerminalRequestArguments,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
pub fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
DebuggerSettings::register(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
pub async fn test_initialize_client(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
cwd: None,
|
||||
},
|
||||
Box::new(|_, _| panic!("Did not expect to hit this code path")),
|
||||
cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap_types::Capabilities {
|
||||
supports_configuration_done_request: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let response = client
|
||||
.request::<Initialize>(InitializeRequestArguments {
|
||||
client_id: Some("zed".to_owned()),
|
||||
client_name: Some("Zed".to_owned()),
|
||||
adapter_id: "fake-adapter".to_owned(),
|
||||
locale: Some("en-US".to_owned()),
|
||||
path_format: Some(InitializeRequestArgumentsPathFormat::Path),
|
||||
supports_variable_type: Some(true),
|
||||
supports_variable_paging: Some(false),
|
||||
supports_run_in_terminal_request: Some(true),
|
||||
supports_memory_references: Some(true),
|
||||
supports_progress_reporting: Some(false),
|
||||
supports_invalidated_event: Some(false),
|
||||
lines_start_at1: Some(true),
|
||||
columns_start_at1: Some(true),
|
||||
supports_memory_event: Some(false),
|
||||
supports_args_can_be_interpreted_by_shell: Some(false),
|
||||
supports_start_debugging_request: Some(true),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
dap_types::Capabilities {
|
||||
supports_configuration_done_request: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
response
|
||||
);
|
||||
|
||||
client.shutdown().await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
pub async fn test_calls_event_handler(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let called_event_handler = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
cwd: None,
|
||||
},
|
||||
Box::new({
|
||||
let called_event_handler = called_event_handler.clone();
|
||||
move |event, _| {
|
||||
called_event_handler.store(true, Ordering::SeqCst);
|
||||
|
||||
assert_eq!(
|
||||
Message::Event(Box::new(Events::Initialized(
|
||||
Some(Capabilities::default())
|
||||
))),
|
||||
event
|
||||
);
|
||||
}
|
||||
}),
|
||||
cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
client
|
||||
.fake_event(Events::Initialized(Some(Capabilities::default())))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
called_event_handler.load(std::sync::atomic::Ordering::SeqCst),
|
||||
"Event handler was not called"
|
||||
);
|
||||
|
||||
client.shutdown().await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
pub async fn test_calls_event_handler_for_reverse_request(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let called_event_handler = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let client = DebugAdapterClient::start(
|
||||
crate::client::SessionId(1),
|
||||
DebugAdapterBinary {
|
||||
command: "command".into(),
|
||||
arguments: Default::default(),
|
||||
envs: Default::default(),
|
||||
connection: None,
|
||||
cwd: None,
|
||||
},
|
||||
Box::new({
|
||||
let called_event_handler = called_event_handler.clone();
|
||||
move |event, _| {
|
||||
called_event_handler.store(true, Ordering::SeqCst);
|
||||
|
||||
assert_eq!(
|
||||
Message::Request(dap_types::messages::Request {
|
||||
seq: 1,
|
||||
command: RunInTerminal::COMMAND.into(),
|
||||
arguments: Some(json!({
|
||||
"cwd": "/project/path/src",
|
||||
"args": ["node", "test.js"],
|
||||
}))
|
||||
}),
|
||||
event
|
||||
);
|
||||
}
|
||||
}),
|
||||
cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
client
|
||||
.fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
|
||||
kind: None,
|
||||
title: None,
|
||||
cwd: "/project/path/src".into(),
|
||||
args: vec!["node".into(), "test.js".into()],
|
||||
env: None,
|
||||
args_can_be_interpreted_by_shell: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
called_event_handler.load(std::sync::atomic::Ordering::SeqCst),
|
||||
"Event handler was not called"
|
||||
);
|
||||
|
||||
client.shutdown().await.unwrap();
|
||||
}
|
||||
}
|
||||
59
crates/dap/src/debugger_settings.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use dap_types::SteppingGranularity;
|
||||
use gpui::{App, Global};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
|
||||
#[serde(default)]
|
||||
pub struct DebuggerSettings {
|
||||
/// Determines the stepping granularity.
|
||||
///
|
||||
/// Default: line
|
||||
pub stepping_granularity: SteppingGranularity,
|
||||
/// Whether the breakpoints should be reused across Zed sessions.
|
||||
///
|
||||
/// Default: true
|
||||
pub save_breakpoints: bool,
|
||||
/// Whether to show the debug button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: bool,
|
||||
/// Time in milliseconds until timeout error when connecting to a TCP debug adapter
|
||||
///
|
||||
/// Default: 2000ms
|
||||
pub timeout: u64,
|
||||
/// Whether to log messages between active debug adapters and Zed
|
||||
///
|
||||
/// Default: true
|
||||
pub log_dap_communications: bool,
|
||||
/// Whether to format dap messages in when adding them to debug adapter logger
|
||||
///
|
||||
/// Default: true
|
||||
pub format_dap_log_messages: bool,
|
||||
}
|
||||
|
||||
impl Default for DebuggerSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
button: true,
|
||||
save_breakpoints: true,
|
||||
stepping_granularity: SteppingGranularity::Line,
|
||||
timeout: 2000,
|
||||
log_dap_communications: true,
|
||||
format_dap_log_messages: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Settings for DebuggerSettings {
|
||||
const KEY: Option<&'static str> = Some("debugger");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
|
||||
sources.json_merge()
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for DebuggerSettings {}
|
||||
28
crates/dap/src/lib.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
pub mod adapters;
|
||||
pub mod client;
|
||||
pub mod debugger_settings;
|
||||
pub mod proto_conversions;
|
||||
pub mod transport;
|
||||
|
||||
pub use dap_types::*;
|
||||
pub use task::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
|
||||
|
||||
pub type ScopeId = u64;
|
||||
pub type VariableReference = u64;
|
||||
pub type StackFrameId = u64;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use adapters::FakeAdapter;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test_config() -> DebugAdapterConfig {
|
||||
DebugAdapterConfig {
|
||||
label: "test config".into(),
|
||||
kind: DebugAdapterKind::Fake,
|
||||
request: DebugRequestType::Launch,
|
||||
program: None,
|
||||
supports_attach: false,
|
||||
cwd: None,
|
||||
initialize_args: None,
|
||||
}
|
||||
}
|
||||
638
crates/dap/src/proto_conversions.rs
Normal file
@@ -0,0 +1,638 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::proto::{
|
||||
self, DapChecksum, DapChecksumAlgorithm, DapEvaluateContext, DapModule, DapScope,
|
||||
DapScopePresentationHint, DapSource, DapSourcePresentationHint, DapStackFrame, DapVariable,
|
||||
SetDebugClientCapabilities,
|
||||
};
|
||||
use dap_types::{
|
||||
Capabilities, OutputEventCategory, OutputEventGroup, ScopePresentationHint, Source,
|
||||
};
|
||||
|
||||
pub trait ProtoConversion {
|
||||
type ProtoType;
|
||||
type Output;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType;
|
||||
fn from_proto(payload: Self::ProtoType) -> Self::Output;
|
||||
}
|
||||
|
||||
impl<T> ProtoConversion for Vec<T>
|
||||
where
|
||||
T: ProtoConversion<Output = T>,
|
||||
{
|
||||
type ProtoType = Vec<T::ProtoType>;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
self.iter().map(|item| item.to_proto()).collect()
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
payload
|
||||
.into_iter()
|
||||
.map(|item| T::from_proto(item))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::Scope {
|
||||
type ProtoType = DapScope;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
Self::ProtoType {
|
||||
name: self.name.clone(),
|
||||
presentation_hint: self
|
||||
.presentation_hint
|
||||
.as_ref()
|
||||
.map(|hint| hint.to_proto().into()),
|
||||
variables_reference: self.variables_reference,
|
||||
named_variables: self.named_variables,
|
||||
indexed_variables: self.indexed_variables,
|
||||
expensive: self.expensive,
|
||||
source: self.source.as_ref().map(Source::to_proto),
|
||||
line: self.line,
|
||||
end_line: self.end_line,
|
||||
column: self.column,
|
||||
end_column: self.end_column,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
let presentation_hint = payload
|
||||
.presentation_hint
|
||||
.and_then(DapScopePresentationHint::from_i32);
|
||||
Self {
|
||||
name: payload.name,
|
||||
presentation_hint: presentation_hint.map(ScopePresentationHint::from_proto),
|
||||
variables_reference: payload.variables_reference,
|
||||
named_variables: payload.named_variables,
|
||||
indexed_variables: payload.indexed_variables,
|
||||
expensive: payload.expensive,
|
||||
source: payload.source.map(dap_types::Source::from_proto),
|
||||
line: payload.line,
|
||||
end_line: payload.end_line,
|
||||
column: payload.column,
|
||||
end_column: payload.end_column,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::Variable {
|
||||
type ProtoType = DapVariable;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
Self::ProtoType {
|
||||
name: self.name.clone(),
|
||||
value: self.value.clone(),
|
||||
r#type: self.type_.clone(),
|
||||
evaluate_name: self.evaluate_name.clone(),
|
||||
variables_reference: self.variables_reference,
|
||||
named_variables: self.named_variables,
|
||||
indexed_variables: self.indexed_variables,
|
||||
memory_reference: self.memory_reference.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
Self {
|
||||
name: payload.name,
|
||||
value: payload.value,
|
||||
type_: payload.r#type,
|
||||
evaluate_name: payload.evaluate_name,
|
||||
presentation_hint: None, // TODO Debugger Collab Add this
|
||||
variables_reference: payload.variables_reference,
|
||||
named_variables: payload.named_variables,
|
||||
indexed_variables: payload.indexed_variables,
|
||||
memory_reference: payload.memory_reference,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::ScopePresentationHint {
|
||||
type ProtoType = DapScopePresentationHint;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
match self {
|
||||
dap_types::ScopePresentationHint::Locals => DapScopePresentationHint::Locals,
|
||||
dap_types::ScopePresentationHint::Arguments => DapScopePresentationHint::Arguments,
|
||||
dap_types::ScopePresentationHint::Registers => DapScopePresentationHint::Registers,
|
||||
dap_types::ScopePresentationHint::ReturnValue => DapScopePresentationHint::ReturnValue,
|
||||
dap_types::ScopePresentationHint::Unknown => DapScopePresentationHint::ScopeUnknown,
|
||||
&_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
match payload {
|
||||
DapScopePresentationHint::Locals => dap_types::ScopePresentationHint::Locals,
|
||||
DapScopePresentationHint::Arguments => dap_types::ScopePresentationHint::Arguments,
|
||||
DapScopePresentationHint::Registers => dap_types::ScopePresentationHint::Registers,
|
||||
DapScopePresentationHint::ReturnValue => dap_types::ScopePresentationHint::ReturnValue,
|
||||
DapScopePresentationHint::ScopeUnknown => dap_types::ScopePresentationHint::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::SourcePresentationHint {
|
||||
type ProtoType = DapSourcePresentationHint;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
match self {
|
||||
dap_types::SourcePresentationHint::Normal => DapSourcePresentationHint::SourceNormal,
|
||||
dap_types::SourcePresentationHint::Emphasize => DapSourcePresentationHint::Emphasize,
|
||||
dap_types::SourcePresentationHint::Deemphasize => {
|
||||
DapSourcePresentationHint::Deemphasize
|
||||
}
|
||||
dap_types::SourcePresentationHint::Unknown => DapSourcePresentationHint::SourceUnknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
match payload {
|
||||
DapSourcePresentationHint::SourceNormal => dap_types::SourcePresentationHint::Normal,
|
||||
DapSourcePresentationHint::Emphasize => dap_types::SourcePresentationHint::Emphasize,
|
||||
DapSourcePresentationHint::Deemphasize => {
|
||||
dap_types::SourcePresentationHint::Deemphasize
|
||||
}
|
||||
DapSourcePresentationHint::SourceUnknown => dap_types::SourcePresentationHint::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::Checksum {
|
||||
type ProtoType = DapChecksum;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
DapChecksum {
|
||||
algorithm: self.algorithm.to_proto().into(),
|
||||
checksum: self.checksum.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
Self {
|
||||
algorithm: dap_types::ChecksumAlgorithm::from_proto(payload.algorithm()),
|
||||
checksum: payload.checksum,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::ChecksumAlgorithm {
|
||||
type ProtoType = DapChecksumAlgorithm;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
match self {
|
||||
dap_types::ChecksumAlgorithm::Md5 => DapChecksumAlgorithm::Md5,
|
||||
dap_types::ChecksumAlgorithm::Sha1 => DapChecksumAlgorithm::Sha1,
|
||||
dap_types::ChecksumAlgorithm::Sha256 => DapChecksumAlgorithm::Sha256,
|
||||
dap_types::ChecksumAlgorithm::Timestamp => DapChecksumAlgorithm::Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
match payload {
|
||||
DapChecksumAlgorithm::Md5 => dap_types::ChecksumAlgorithm::Md5,
|
||||
DapChecksumAlgorithm::Sha1 => dap_types::ChecksumAlgorithm::Sha1,
|
||||
DapChecksumAlgorithm::Sha256 => dap_types::ChecksumAlgorithm::Sha256,
|
||||
DapChecksumAlgorithm::Timestamp => dap_types::ChecksumAlgorithm::Timestamp,
|
||||
DapChecksumAlgorithm::ChecksumAlgorithmUnspecified => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::Source {
|
||||
type ProtoType = DapSource;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
Self::ProtoType {
|
||||
name: self.name.clone(),
|
||||
path: self.path.clone(),
|
||||
source_reference: self.source_reference,
|
||||
presentation_hint: self.presentation_hint.map(|hint| hint.to_proto().into()),
|
||||
origin: self.origin.clone(),
|
||||
sources: self
|
||||
.sources
|
||||
.clone()
|
||||
.map(|src| src.to_proto())
|
||||
.unwrap_or_default(),
|
||||
adapter_data: Default::default(), // TODO Debugger Collab
|
||||
checksums: self
|
||||
.checksums
|
||||
.clone()
|
||||
.map(|c| c.to_proto())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
Self {
|
||||
name: payload.name.clone(),
|
||||
path: payload.path.clone(),
|
||||
source_reference: payload.source_reference,
|
||||
presentation_hint: payload
|
||||
.presentation_hint
|
||||
.and_then(DapSourcePresentationHint::from_i32)
|
||||
.map(dap_types::SourcePresentationHint::from_proto),
|
||||
origin: payload.origin.clone(),
|
||||
sources: Some(Vec::<dap_types::Source>::from_proto(payload.sources)),
|
||||
checksums: Some(Vec::<dap_types::Checksum>::from_proto(payload.checksums)),
|
||||
adapter_data: None, // TODO Debugger Collab
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::StackFrame {
|
||||
type ProtoType = DapStackFrame;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
Self::ProtoType {
|
||||
id: self.id,
|
||||
name: self.name.clone(),
|
||||
source: self.source.as_ref().map(|src| src.to_proto()),
|
||||
line: self.line,
|
||||
column: self.column,
|
||||
end_line: self.end_line,
|
||||
end_column: self.end_column,
|
||||
can_restart: self.can_restart,
|
||||
instruction_pointer_reference: self.instruction_pointer_reference.clone(),
|
||||
module_id: None, // TODO Debugger Collab
|
||||
presentation_hint: None, // TODO Debugger Collab
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
Self {
|
||||
id: payload.id,
|
||||
name: payload.name,
|
||||
source: payload.source.map(dap_types::Source::from_proto),
|
||||
line: payload.line,
|
||||
column: payload.column,
|
||||
end_line: payload.end_line,
|
||||
end_column: payload.end_column,
|
||||
can_restart: payload.can_restart,
|
||||
instruction_pointer_reference: payload.instruction_pointer_reference,
|
||||
module_id: None, // TODO Debugger Collab
|
||||
presentation_hint: None, // TODO Debugger Collab
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::Module {
|
||||
type ProtoType = DapModule;
|
||||
type Output = Result<Self>;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
let id = match &self.id {
|
||||
dap_types::ModuleId::Number(num) => proto::dap_module_id::Id::Number(*num),
|
||||
dap_types::ModuleId::String(string) => proto::dap_module_id::Id::String(string.clone()),
|
||||
};
|
||||
|
||||
DapModule {
|
||||
id: Some(proto::DapModuleId { id: Some(id) }),
|
||||
name: self.name.clone(),
|
||||
path: self.path.clone(),
|
||||
is_optimized: self.is_optimized,
|
||||
is_user_code: self.is_user_code,
|
||||
version: self.version.clone(),
|
||||
symbol_status: self.symbol_status.clone(),
|
||||
symbol_file_path: self.symbol_file_path.clone(),
|
||||
date_time_stamp: self.date_time_stamp.clone(),
|
||||
address_range: self.address_range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Result<Self> {
|
||||
let id = match payload
|
||||
.id
|
||||
.ok_or(anyhow!("All DapModule proto messages must have an id"))?
|
||||
.id
|
||||
.ok_or(anyhow!("All DapModuleID proto messages must have an id"))?
|
||||
{
|
||||
proto::dap_module_id::Id::String(string) => dap_types::ModuleId::String(string),
|
||||
proto::dap_module_id::Id::Number(num) => dap_types::ModuleId::Number(num),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
id,
|
||||
name: payload.name,
|
||||
path: payload.path,
|
||||
is_optimized: payload.is_optimized,
|
||||
is_user_code: payload.is_user_code,
|
||||
version: payload.version,
|
||||
symbol_status: payload.symbol_status,
|
||||
symbol_file_path: payload.symbol_file_path,
|
||||
date_time_stamp: payload.date_time_stamp,
|
||||
address_range: payload.address_range,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capabilities_from_proto(payload: &SetDebugClientCapabilities) -> Capabilities {
|
||||
Capabilities {
|
||||
supports_loaded_sources_request: Some(payload.supports_loaded_sources_request),
|
||||
supports_modules_request: Some(payload.supports_modules_request),
|
||||
supports_restart_request: Some(payload.supports_restart_request),
|
||||
supports_set_expression: Some(payload.supports_set_expression),
|
||||
supports_single_thread_execution_requests: Some(
|
||||
payload.supports_single_thread_execution_requests,
|
||||
),
|
||||
supports_step_back: Some(payload.supports_step_back),
|
||||
supports_stepping_granularity: Some(payload.supports_stepping_granularity),
|
||||
supports_terminate_threads_request: Some(payload.supports_terminate_threads_request),
|
||||
supports_restart_frame: Some(payload.supports_restart_frame_request),
|
||||
supports_clipboard_context: Some(payload.supports_clipboard_context),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capabilities_to_proto(
|
||||
capabilities: &Capabilities,
|
||||
project_id: u64,
|
||||
session_id: u64,
|
||||
) -> SetDebugClientCapabilities {
|
||||
SetDebugClientCapabilities {
|
||||
session_id,
|
||||
project_id,
|
||||
supports_loaded_sources_request: capabilities
|
||||
.supports_loaded_sources_request
|
||||
.unwrap_or_default(),
|
||||
supports_modules_request: capabilities.supports_modules_request.unwrap_or_default(),
|
||||
supports_restart_request: capabilities.supports_restart_request.unwrap_or_default(),
|
||||
supports_set_expression: capabilities.supports_set_expression.unwrap_or_default(),
|
||||
supports_single_thread_execution_requests: capabilities
|
||||
.supports_single_thread_execution_requests
|
||||
.unwrap_or_default(),
|
||||
supports_step_back: capabilities.supports_step_back.unwrap_or_default(),
|
||||
supports_stepping_granularity: capabilities
|
||||
.supports_stepping_granularity
|
||||
.unwrap_or_default(),
|
||||
supports_terminate_threads_request: capabilities
|
||||
.supports_terminate_threads_request
|
||||
.unwrap_or_default(),
|
||||
supports_restart_frame_request: capabilities.supports_restart_frame.unwrap_or_default(),
|
||||
supports_clipboard_context: capabilities.supports_clipboard_context.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::SteppingGranularity {
|
||||
type ProtoType = proto::SteppingGranularity;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
match self {
|
||||
dap_types::SteppingGranularity::Statement => proto::SteppingGranularity::Statement,
|
||||
dap_types::SteppingGranularity::Line => proto::SteppingGranularity::Line,
|
||||
dap_types::SteppingGranularity::Instruction => proto::SteppingGranularity::Instruction,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
match payload {
|
||||
proto::SteppingGranularity::Line => dap_types::SteppingGranularity::Line,
|
||||
proto::SteppingGranularity::Instruction => dap_types::SteppingGranularity::Instruction,
|
||||
proto::SteppingGranularity::Statement => dap_types::SteppingGranularity::Statement,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::OutputEventCategory {
|
||||
type ProtoType = proto::DapOutputCategory;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
match self {
|
||||
Self::Console => proto::DapOutputCategory::ConsoleOutput,
|
||||
Self::Important => proto::DapOutputCategory::Important,
|
||||
Self::Stdout => proto::DapOutputCategory::Stdout,
|
||||
Self::Stderr => proto::DapOutputCategory::Stderr,
|
||||
_ => proto::DapOutputCategory::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
match payload {
|
||||
proto::DapOutputCategory::ConsoleOutput => Self::Console,
|
||||
proto::DapOutputCategory::Important => Self::Important,
|
||||
proto::DapOutputCategory::Stdout => Self::Stdout,
|
||||
proto::DapOutputCategory::Stderr => Self::Stderr,
|
||||
proto::DapOutputCategory::Unknown => Self::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::OutputEvent {
|
||||
type ProtoType = proto::DapOutputEvent;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
proto::DapOutputEvent {
|
||||
category: self
|
||||
.category
|
||||
.as_ref()
|
||||
.map(|category| category.to_proto().into()),
|
||||
output: self.output.clone(),
|
||||
variables_reference: self.variables_reference,
|
||||
source: self.source.as_ref().map(|source| source.to_proto()),
|
||||
line: self.line.map(|line| line as u32),
|
||||
column: self.column.map(|column| column as u32),
|
||||
group: self.group.map(|group| group.to_proto().into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
dap_types::OutputEvent {
|
||||
category: payload
|
||||
.category
|
||||
.and_then(proto::DapOutputCategory::from_i32)
|
||||
.map(OutputEventCategory::from_proto),
|
||||
output: payload.output.clone(),
|
||||
variables_reference: payload.variables_reference,
|
||||
source: payload.source.map(Source::from_proto),
|
||||
line: payload.line.map(|line| line as u64),
|
||||
column: payload.column.map(|column| column as u64),
|
||||
group: payload
|
||||
.group
|
||||
.and_then(proto::DapOutputEventGroup::from_i32)
|
||||
.map(OutputEventGroup::from_proto),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::OutputEventGroup {
|
||||
type ProtoType = proto::DapOutputEventGroup;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
match self {
|
||||
dap_types::OutputEventGroup::Start => proto::DapOutputEventGroup::Start,
|
||||
dap_types::OutputEventGroup::StartCollapsed => {
|
||||
proto::DapOutputEventGroup::StartCollapsed
|
||||
}
|
||||
dap_types::OutputEventGroup::End => proto::DapOutputEventGroup::End,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
match payload {
|
||||
proto::DapOutputEventGroup::Start => Self::Start,
|
||||
proto::DapOutputEventGroup::StartCollapsed => Self::StartCollapsed,
|
||||
proto::DapOutputEventGroup::End => Self::End,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::CompletionItem {
|
||||
type ProtoType = proto::DapCompletionItem;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
proto::DapCompletionItem {
|
||||
label: self.label.clone(),
|
||||
text: self.text.clone(),
|
||||
detail: self.detail.clone(),
|
||||
typ: self
|
||||
.type_
|
||||
.as_ref()
|
||||
.map(ProtoConversion::to_proto)
|
||||
.map(|typ| typ.into()),
|
||||
start: self.start,
|
||||
length: self.length,
|
||||
selection_start: self.selection_start,
|
||||
selection_length: self.selection_length,
|
||||
sort_text: self.sort_text.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
let typ = payload.typ(); // todo(debugger): This might be a potential issue/bug because it defaults to a type when it's None
|
||||
|
||||
Self {
|
||||
label: payload.label,
|
||||
detail: payload.detail,
|
||||
sort_text: payload.sort_text,
|
||||
text: payload.text.clone(),
|
||||
type_: Some(dap_types::CompletionItemType::from_proto(typ)),
|
||||
start: payload.start,
|
||||
length: payload.length,
|
||||
selection_start: payload.selection_start,
|
||||
selection_length: payload.selection_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::EvaluateArgumentsContext {
|
||||
type ProtoType = DapEvaluateContext;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
match self {
|
||||
dap_types::EvaluateArgumentsContext::Variables => {
|
||||
proto::DapEvaluateContext::EvaluateVariables
|
||||
}
|
||||
dap_types::EvaluateArgumentsContext::Watch => proto::DapEvaluateContext::Watch,
|
||||
dap_types::EvaluateArgumentsContext::Hover => proto::DapEvaluateContext::Hover,
|
||||
dap_types::EvaluateArgumentsContext::Repl => proto::DapEvaluateContext::Repl,
|
||||
dap_types::EvaluateArgumentsContext::Clipboard => proto::DapEvaluateContext::Clipboard,
|
||||
dap_types::EvaluateArgumentsContext::Unknown => {
|
||||
proto::DapEvaluateContext::EvaluateUnknown
|
||||
}
|
||||
_ => proto::DapEvaluateContext::EvaluateUnknown,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
match payload {
|
||||
proto::DapEvaluateContext::EvaluateVariables => {
|
||||
dap_types::EvaluateArgumentsContext::Variables
|
||||
}
|
||||
proto::DapEvaluateContext::Watch => dap_types::EvaluateArgumentsContext::Watch,
|
||||
proto::DapEvaluateContext::Hover => dap_types::EvaluateArgumentsContext::Hover,
|
||||
proto::DapEvaluateContext::Repl => dap_types::EvaluateArgumentsContext::Repl,
|
||||
proto::DapEvaluateContext::Clipboard => dap_types::EvaluateArgumentsContext::Clipboard,
|
||||
proto::DapEvaluateContext::EvaluateUnknown => {
|
||||
dap_types::EvaluateArgumentsContext::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::CompletionItemType {
|
||||
type ProtoType = proto::DapCompletionItemType;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
match self {
|
||||
dap_types::CompletionItemType::Class => proto::DapCompletionItemType::Class,
|
||||
dap_types::CompletionItemType::Color => proto::DapCompletionItemType::Color,
|
||||
dap_types::CompletionItemType::Constructor => proto::DapCompletionItemType::Constructor,
|
||||
dap_types::CompletionItemType::Customcolor => proto::DapCompletionItemType::Customcolor,
|
||||
dap_types::CompletionItemType::Enum => proto::DapCompletionItemType::Enum,
|
||||
dap_types::CompletionItemType::Field => proto::DapCompletionItemType::Field,
|
||||
dap_types::CompletionItemType::File => proto::DapCompletionItemType::CompletionItemFile,
|
||||
dap_types::CompletionItemType::Function => proto::DapCompletionItemType::Function,
|
||||
dap_types::CompletionItemType::Interface => proto::DapCompletionItemType::Interface,
|
||||
dap_types::CompletionItemType::Keyword => proto::DapCompletionItemType::Keyword,
|
||||
dap_types::CompletionItemType::Method => proto::DapCompletionItemType::Method,
|
||||
dap_types::CompletionItemType::Module => proto::DapCompletionItemType::Module,
|
||||
dap_types::CompletionItemType::Property => proto::DapCompletionItemType::Property,
|
||||
dap_types::CompletionItemType::Reference => proto::DapCompletionItemType::Reference,
|
||||
dap_types::CompletionItemType::Snippet => proto::DapCompletionItemType::Snippet,
|
||||
dap_types::CompletionItemType::Text => proto::DapCompletionItemType::Text,
|
||||
dap_types::CompletionItemType::Unit => proto::DapCompletionItemType::Unit,
|
||||
dap_types::CompletionItemType::Value => proto::DapCompletionItemType::Value,
|
||||
dap_types::CompletionItemType::Variable => proto::DapCompletionItemType::Variable,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
match payload {
|
||||
proto::DapCompletionItemType::Class => dap_types::CompletionItemType::Class,
|
||||
proto::DapCompletionItemType::Color => dap_types::CompletionItemType::Color,
|
||||
proto::DapCompletionItemType::CompletionItemFile => dap_types::CompletionItemType::File,
|
||||
proto::DapCompletionItemType::Constructor => dap_types::CompletionItemType::Constructor,
|
||||
proto::DapCompletionItemType::Customcolor => dap_types::CompletionItemType::Customcolor,
|
||||
proto::DapCompletionItemType::Enum => dap_types::CompletionItemType::Enum,
|
||||
proto::DapCompletionItemType::Field => dap_types::CompletionItemType::Field,
|
||||
proto::DapCompletionItemType::Function => dap_types::CompletionItemType::Function,
|
||||
proto::DapCompletionItemType::Interface => dap_types::CompletionItemType::Interface,
|
||||
proto::DapCompletionItemType::Keyword => dap_types::CompletionItemType::Keyword,
|
||||
proto::DapCompletionItemType::Method => dap_types::CompletionItemType::Method,
|
||||
proto::DapCompletionItemType::Module => dap_types::CompletionItemType::Module,
|
||||
proto::DapCompletionItemType::Property => dap_types::CompletionItemType::Property,
|
||||
proto::DapCompletionItemType::Reference => dap_types::CompletionItemType::Reference,
|
||||
proto::DapCompletionItemType::Snippet => dap_types::CompletionItemType::Snippet,
|
||||
proto::DapCompletionItemType::Text => dap_types::CompletionItemType::Text,
|
||||
proto::DapCompletionItemType::Unit => dap_types::CompletionItemType::Unit,
|
||||
proto::DapCompletionItemType::Value => dap_types::CompletionItemType::Value,
|
||||
proto::DapCompletionItemType::Variable => dap_types::CompletionItemType::Variable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoConversion for dap_types::Thread {
|
||||
type ProtoType = proto::DapThread;
|
||||
type Output = Self;
|
||||
|
||||
fn to_proto(&self) -> Self::ProtoType {
|
||||
proto::DapThread {
|
||||
id: self.id,
|
||||
name: self.name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(payload: Self::ProtoType) -> Self {
|
||||
Self {
|
||||
id: payload.id,
|
||||
name: payload.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
882
crates/dap/src/transport.rs
Normal file
@@ -0,0 +1,882 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dap_types::{
|
||||
messages::{Message, Response},
|
||||
ErrorResponse,
|
||||
};
|
||||
use futures::{channel::oneshot, select, AsyncRead, AsyncReadExt as _, AsyncWrite, FutureExt as _};
|
||||
use gpui::AsyncApp;
|
||||
use settings::Settings as _;
|
||||
use smallvec::SmallVec;
|
||||
use smol::{
|
||||
channel::{unbounded, Receiver, Sender},
|
||||
io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
|
||||
lock::Mutex,
|
||||
net::{TcpListener, TcpStream},
|
||||
process::Child,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{Ipv4Addr, SocketAddrV4},
|
||||
process::Stdio,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use task::TCPHost;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
|
||||
|
||||
pub type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
pub enum LogKind {
|
||||
Adapter,
|
||||
Rpc,
|
||||
}
|
||||
|
||||
pub enum IoKind {
|
||||
StdIn,
|
||||
StdOut,
|
||||
StdErr,
|
||||
}
|
||||
|
||||
pub struct TransportPipe {
|
||||
input: Box<dyn AsyncWrite + Unpin + Send + 'static>,
|
||||
output: Box<dyn AsyncRead + Unpin + Send + 'static>,
|
||||
stdout: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
|
||||
stderr: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
|
||||
}
|
||||
|
||||
impl TransportPipe {
|
||||
pub fn new(
|
||||
input: Box<dyn AsyncWrite + Unpin + Send + 'static>,
|
||||
output: Box<dyn AsyncRead + Unpin + Send + 'static>,
|
||||
stdout: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
|
||||
stderr: Option<Box<dyn AsyncRead + Unpin + Send + 'static>>,
|
||||
) -> Self {
|
||||
TransportPipe {
|
||||
input,
|
||||
output,
|
||||
stdout,
|
||||
stderr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Requests = Arc<Mutex<HashMap<u64, oneshot::Sender<Result<Response>>>>>;
|
||||
type LogHandlers = Arc<parking_lot::Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>;
|
||||
|
||||
pub enum Transport {
|
||||
Stdio(StdioTransport),
|
||||
Tcp(TcpTransport),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Fake(FakeTransport),
|
||||
}
|
||||
|
||||
impl Transport {
|
||||
async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
return FakeTransport::start(cx)
|
||||
.await
|
||||
.map(|(transports, fake)| (transports, Self::Fake(fake)));
|
||||
|
||||
if binary.connection.is_some() {
|
||||
TcpTransport::start(binary, cx)
|
||||
.await
|
||||
.map(|(transports, tcp)| (transports, Self::Tcp(tcp)))
|
||||
} else {
|
||||
StdioTransport::start(binary, cx)
|
||||
.await
|
||||
.map(|(transports, stdio)| (transports, Self::Stdio(stdio)))
|
||||
}
|
||||
}
|
||||
|
||||
fn has_adapter_logs(&self) -> bool {
|
||||
match self {
|
||||
Transport::Stdio(stdio_transport) => stdio_transport.has_adapter_logs(),
|
||||
Transport::Tcp(tcp_transport) => tcp_transport.has_adapter_logs(),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Transport::Fake(fake_transport) => fake_transport.has_adapter_logs(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
match self {
|
||||
Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
|
||||
Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Transport::Fake(fake_transport) => fake_transport.kill().await,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn as_fake(&self) -> &FakeTransport {
|
||||
match self {
|
||||
Transport::Fake(fake_transport) => fake_transport,
|
||||
_ => panic!("Not a fake transport layer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TransportDelegate {
|
||||
log_handlers: LogHandlers,
|
||||
current_requests: Requests,
|
||||
pending_requests: Requests,
|
||||
transport: Transport,
|
||||
server_tx: Arc<Mutex<Option<Sender<Message>>>>,
|
||||
}
|
||||
|
||||
impl TransportDelegate {
|
||||
pub(crate) async fn start(
|
||||
binary: &DebugAdapterBinary,
|
||||
cx: AsyncApp,
|
||||
) -> Result<((Receiver<Message>, Sender<Message>), Self)> {
|
||||
let (transport_pipes, transport) = Transport::start(binary, cx.clone()).await?;
|
||||
let mut this = Self {
|
||||
transport,
|
||||
server_tx: Default::default(),
|
||||
log_handlers: Default::default(),
|
||||
current_requests: Default::default(),
|
||||
pending_requests: Default::default(),
|
||||
};
|
||||
let messages = this.start_handlers(transport_pipes, cx).await?;
|
||||
Ok((messages, this))
|
||||
}
|
||||
|
||||
async fn start_handlers(
|
||||
&mut self,
|
||||
mut params: TransportPipe,
|
||||
cx: AsyncApp,
|
||||
) -> Result<(Receiver<Message>, Sender<Message>)> {
|
||||
let (client_tx, server_rx) = unbounded::<Message>();
|
||||
let (server_tx, client_rx) = unbounded::<Message>();
|
||||
|
||||
let log_dap_communications =
|
||||
cx.update(|cx| DebuggerSettings::get_global(cx).log_dap_communications)
|
||||
.with_context(|| "Failed to get Debugger Setting log dap communications error in transport::start_handlers. Defaulting to false")
|
||||
.unwrap_or(false);
|
||||
|
||||
let log_handler = if log_dap_communications {
|
||||
Some(self.log_handlers.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
cx.update(|cx| {
|
||||
if let Some(stdout) = params.stdout.take() {
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_adapter_log(stdout, log_handler.clone()))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_output(
|
||||
params.output,
|
||||
client_tx,
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
))
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
if let Some(stderr) = params.stderr.take() {
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_error(stderr, self.log_handlers.clone()))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::handle_input(
|
||||
params.input,
|
||||
client_rx,
|
||||
self.current_requests.clone(),
|
||||
self.pending_requests.clone(),
|
||||
log_handler.clone(),
|
||||
))
|
||||
.detach_and_log_err(cx);
|
||||
})?;
|
||||
|
||||
{
|
||||
let mut lock = self.server_tx.lock().await;
|
||||
*lock = Some(server_tx.clone());
|
||||
}
|
||||
|
||||
Ok((server_rx, server_tx))
|
||||
}
|
||||
|
||||
pub(crate) async fn add_pending_request(
|
||||
&self,
|
||||
sequence_id: u64,
|
||||
request: oneshot::Sender<Result<Response>>,
|
||||
) {
|
||||
let mut pending_requests = self.pending_requests.lock().await;
|
||||
pending_requests.insert(sequence_id, request);
|
||||
}
|
||||
|
||||
pub(crate) async fn cancel_pending_request(&self, sequence_id: &u64) {
|
||||
let mut pending_requests = self.pending_requests.lock().await;
|
||||
pending_requests.remove(sequence_id);
|
||||
}
|
||||
|
||||
pub(crate) async fn send_message(&self, message: Message) -> Result<()> {
|
||||
if let Some(server_tx) = self.server_tx.lock().await.as_ref() {
|
||||
server_tx
|
||||
.send(message)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to send message: {}", e))
|
||||
} else {
|
||||
Err(anyhow!("Server tx already dropped"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_adapter_log<Stdout>(
|
||||
stdout: Stdout,
|
||||
log_handlers: Option<LogHandlers>,
|
||||
) -> Result<()>
|
||||
where
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
let mut reader = BufReader::new(stdout);
|
||||
let mut line = String::new();
|
||||
|
||||
let result = loop {
|
||||
line.truncate(0);
|
||||
|
||||
let bytes_read = match reader.read_line(&mut line).await {
|
||||
Ok(bytes_read) => bytes_read,
|
||||
Err(e) => break Err(e.into()),
|
||||
};
|
||||
|
||||
if bytes_read == 0 {
|
||||
break Err(anyhow!("Debugger log stream closed"));
|
||||
}
|
||||
|
||||
if let Some(log_handlers) = log_handlers.as_ref() {
|
||||
for (kind, handler) in log_handlers.lock().iter_mut() {
|
||||
if matches!(kind, LogKind::Adapter) {
|
||||
handler(IoKind::StdOut, line.as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
smol::future::yield_now().await;
|
||||
};
|
||||
|
||||
log::debug!("Handle adapter log dropped");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn build_rpc_message(message: String) -> String {
|
||||
format!("Content-Length: {}\r\n\r\n{}", message.len(), message)
|
||||
}
|
||||
|
||||
async fn handle_input<Stdin>(
|
||||
mut server_stdin: Stdin,
|
||||
client_rx: Receiver<Message>,
|
||||
current_requests: Requests,
|
||||
pending_requests: Requests,
|
||||
log_handlers: Option<LogHandlers>,
|
||||
) -> Result<()>
|
||||
where
|
||||
Stdin: AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let result = loop {
|
||||
match client_rx.recv().await {
|
||||
Ok(message) => {
|
||||
if let Message::Request(request) = &message {
|
||||
if let Some(sender) = current_requests.lock().await.remove(&request.seq) {
|
||||
pending_requests.lock().await.insert(request.seq, sender);
|
||||
}
|
||||
}
|
||||
|
||||
let message = match serde_json::to_string(&message) {
|
||||
Ok(message) => message,
|
||||
Err(e) => break Err(e.into()),
|
||||
};
|
||||
|
||||
if let Some(log_handlers) = log_handlers.as_ref() {
|
||||
for (kind, log_handler) in log_handlers.lock().iter_mut() {
|
||||
if matches!(kind, LogKind::Rpc) {
|
||||
log_handler(IoKind::StdIn, &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = server_stdin
|
||||
.write_all(Self::build_rpc_message(message).as_bytes())
|
||||
.await
|
||||
{
|
||||
break Err(e.into());
|
||||
}
|
||||
|
||||
if let Err(e) = server_stdin.flush().await {
|
||||
break Err(e.into());
|
||||
}
|
||||
}
|
||||
Err(error) => break Err(error.into()),
|
||||
}
|
||||
|
||||
smol::future::yield_now().await;
|
||||
};
|
||||
|
||||
log::debug!("Handle adapter input dropped");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn handle_output<Stdout>(
|
||||
server_stdout: Stdout,
|
||||
client_tx: Sender<Message>,
|
||||
pending_requests: Requests,
|
||||
log_handlers: Option<LogHandlers>,
|
||||
) -> Result<()>
|
||||
where
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
let mut recv_buffer = String::new();
|
||||
let mut reader = BufReader::new(server_stdout);
|
||||
|
||||
let result = loop {
|
||||
let message =
|
||||
Self::receive_server_message(&mut reader, &mut recv_buffer, log_handlers.as_ref())
|
||||
.await;
|
||||
|
||||
match message {
|
||||
Ok(Message::Response(res)) => {
|
||||
if let Some(tx) = pending_requests.lock().await.remove(&res.request_seq) {
|
||||
if let Err(e) = tx.send(Self::process_response(res)) {
|
||||
break Err(anyhow!("Failed to send response: {:?}", e));
|
||||
}
|
||||
} else {
|
||||
client_tx.send(Message::Response(res)).await?;
|
||||
};
|
||||
}
|
||||
Ok(message) => {
|
||||
client_tx.send(message).await?;
|
||||
}
|
||||
Err(e) => break Err(e),
|
||||
}
|
||||
|
||||
smol::future::yield_now().await;
|
||||
};
|
||||
|
||||
drop(client_tx);
|
||||
|
||||
log::debug!("Handle adapter output dropped");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn handle_error<Stderr>(stderr: Stderr, log_handlers: LogHandlers) -> Result<()>
|
||||
where
|
||||
Stderr: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
let mut buffer = String::new();
|
||||
|
||||
let mut reader = BufReader::new(stderr);
|
||||
|
||||
let result = loop {
|
||||
match reader.read_line(&mut buffer).await {
|
||||
Ok(0) => break Err(anyhow!("debugger error stream closed")),
|
||||
Ok(_) => {
|
||||
for (kind, log_handler) in log_handlers.lock().iter_mut() {
|
||||
if matches!(kind, LogKind::Adapter) {
|
||||
log_handler(IoKind::StdErr, buffer.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
buffer.truncate(0);
|
||||
}
|
||||
Err(error) => break Err(error.into()),
|
||||
}
|
||||
|
||||
smol::future::yield_now().await;
|
||||
};
|
||||
|
||||
log::debug!("Handle adapter error dropped");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn process_response(response: Response) -> Result<Response> {
|
||||
if response.success {
|
||||
Ok(response)
|
||||
} else {
|
||||
if let Some(body) = response.body.clone() {
|
||||
if let Ok(error) = serde_json::from_value::<ErrorResponse>(body) {
|
||||
if let Some(message) = error.error {
|
||||
return Err(anyhow!(message.format));
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"Received error response from adapter. Response: {:?}",
|
||||
response.clone()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn receive_server_message<Stdout>(
|
||||
reader: &mut BufReader<Stdout>,
|
||||
buffer: &mut String,
|
||||
log_handlers: Option<&LogHandlers>,
|
||||
) -> Result<Message>
|
||||
where
|
||||
Stdout: AsyncRead + Unpin + Send + 'static,
|
||||
{
|
||||
let mut content_length = None;
|
||||
loop {
|
||||
buffer.truncate(0);
|
||||
|
||||
if reader
|
||||
.read_line(buffer)
|
||||
.await
|
||||
.with_context(|| "reading a message from server")?
|
||||
== 0
|
||||
{
|
||||
return Err(anyhow!("debugger reader stream closed"));
|
||||
};
|
||||
|
||||
if buffer == "\r\n" {
|
||||
break;
|
||||
}
|
||||
|
||||
let parts = buffer.trim().split_once(": ");
|
||||
|
||||
match parts {
|
||||
Some(("Content-Length", value)) => {
|
||||
content_length = Some(value.parse().context("invalid content length")?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let content_length = content_length.context("missing content length")?;
|
||||
|
||||
let mut content = vec![0; content_length];
|
||||
reader
|
||||
.read_exact(&mut content)
|
||||
.await
|
||||
.with_context(|| "reading after a loop")?;
|
||||
|
||||
let message = std::str::from_utf8(&content).context("invalid utf8 from server")?;
|
||||
|
||||
if let Some(log_handlers) = log_handlers {
|
||||
for (kind, log_handler) in log_handlers.lock().iter_mut() {
|
||||
if matches!(kind, LogKind::Rpc) {
|
||||
log_handler(IoKind::StdOut, &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::from_str::<Message>(message)?)
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
log::debug!("Start shutdown client");
|
||||
|
||||
if let Some(server_tx) = self.server_tx.lock().await.take().as_ref() {
|
||||
server_tx.close();
|
||||
}
|
||||
|
||||
let mut current_requests = self.current_requests.lock().await;
|
||||
let mut pending_requests = self.pending_requests.lock().await;
|
||||
|
||||
current_requests.clear();
|
||||
pending_requests.clear();
|
||||
|
||||
let _ = self.transport.kill().await.log_err();
|
||||
|
||||
drop(current_requests);
|
||||
drop(pending_requests);
|
||||
|
||||
log::debug!("Shutdown client completed");
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
pub fn has_adapter_logs(&self) -> bool {
|
||||
self.transport.has_adapter_logs()
|
||||
}
|
||||
|
||||
pub fn transport(&self) -> &Transport {
|
||||
&self.transport
|
||||
}
|
||||
|
||||
pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
|
||||
where
|
||||
F: 'static + Send + FnMut(IoKind, &str),
|
||||
{
|
||||
let mut log_handlers = self.log_handlers.lock();
|
||||
log_handlers.push((kind, Box::new(f)));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TcpTransport {
|
||||
pub port: u16,
|
||||
pub host: Ipv4Addr,
|
||||
pub timeout: u64,
|
||||
process: Mutex<Child>,
|
||||
}
|
||||
|
||||
impl TcpTransport {
|
||||
/// Get an open port to use with the tcp client when not supplied by debug config
|
||||
pub async fn port(host: &TCPHost) -> Result<u16> {
|
||||
if let Some(port) = host.port {
|
||||
Ok(port)
|
||||
} else {
|
||||
Ok(TcpListener::bind(SocketAddrV4::new(host.host(), 0))
|
||||
.await?
|
||||
.local_addr()?
|
||||
.port())
|
||||
}
|
||||
}
|
||||
|
||||
async fn start(binary: &DebugAdapterBinary, cx: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
let Some(connection_args) = binary.connection.as_ref() else {
|
||||
return Err(anyhow!("No connection arguments provided"));
|
||||
};
|
||||
|
||||
let host = connection_args.host;
|
||||
let port = connection_args.port;
|
||||
|
||||
let mut command = util::command::new_smol_command(&binary.command);
|
||||
|
||||
if let Some(cwd) = &binary.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
|
||||
if let Some(args) = &binary.arguments {
|
||||
command.args(args);
|
||||
}
|
||||
|
||||
if let Some(envs) = &binary.envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
|
||||
command
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to start debug adapter.")?;
|
||||
|
||||
let address = SocketAddrV4::new(host, port);
|
||||
|
||||
let timeout = connection_args.timeout.unwrap_or_else(|| {
|
||||
cx.update(|cx| DebuggerSettings::get_global(cx).timeout)
|
||||
.unwrap_or(2000u64)
|
||||
});
|
||||
|
||||
let (rx, tx) = select! {
|
||||
_ = cx.background_executor().timer(Duration::from_millis(timeout)).fuse() => {
|
||||
return Err(anyhow!(format!("Connection to TCP DAP timeout {}:{}", host, port)))
|
||||
},
|
||||
result = cx.spawn(|cx| async move {
|
||||
loop {
|
||||
match TcpStream::connect(address).await {
|
||||
Ok(stream) => return stream.split(),
|
||||
Err(_) => {
|
||||
cx.background_executor().timer(Duration::from_millis(100)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}).fuse() => result
|
||||
};
|
||||
log::info!(
|
||||
"Debug adapter has connected to TCP server {}:{}",
|
||||
host,
|
||||
port
|
||||
);
|
||||
let stdout = process.stdout.take();
|
||||
let stderr = process.stderr.take();
|
||||
|
||||
let this = Self {
|
||||
port,
|
||||
host,
|
||||
process: Mutex::new(process),
|
||||
timeout,
|
||||
};
|
||||
|
||||
let pipe = TransportPipe::new(
|
||||
Box::new(tx),
|
||||
Box::new(BufReader::new(rx)),
|
||||
stdout.map(|s| Box::new(s) as Box<dyn AsyncRead + Unpin + Send>),
|
||||
stderr.map(|s| Box::new(s) as Box<dyn AsyncRead + Unpin + Send>),
|
||||
);
|
||||
|
||||
Ok((pipe, this))
|
||||
}
|
||||
|
||||
fn has_adapter_logs(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
self.process.lock().await.kill()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StdioTransport {
|
||||
process: Mutex<Child>,
|
||||
}
|
||||
|
||||
impl StdioTransport {
|
||||
async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
let mut command = util::command::new_smol_command(&binary.command);
|
||||
|
||||
if let Some(cwd) = &binary.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
|
||||
if let Some(args) = &binary.arguments {
|
||||
command.args(args);
|
||||
}
|
||||
|
||||
if let Some(envs) = &binary.envs {
|
||||
command.envs(envs);
|
||||
}
|
||||
|
||||
command
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut process = command
|
||||
.spawn()
|
||||
.with_context(|| "failed to spawn command.")?;
|
||||
|
||||
let stdin = process
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stdin"))?;
|
||||
let stdout = process
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("Failed to open stdout"))?;
|
||||
let stderr = process
|
||||
.stdout
|
||||
.take()
|
||||
.map(|io_err| Box::new(io_err) as Box<dyn AsyncRead + Unpin + Send>);
|
||||
|
||||
if stderr.is_none() {
|
||||
log::error!(
|
||||
"Failed to connect to stderr for debug adapter command {}",
|
||||
&binary.command
|
||||
);
|
||||
}
|
||||
|
||||
log::info!("Debug adapter has connected to stdio adapter");
|
||||
|
||||
let process = Mutex::new(process);
|
||||
|
||||
Ok((
|
||||
TransportPipe::new(
|
||||
Box::new(stdin),
|
||||
Box::new(BufReader::new(stdout)),
|
||||
None,
|
||||
stderr,
|
||||
),
|
||||
Self { process },
|
||||
))
|
||||
}
|
||||
|
||||
fn has_adapter_logs(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
self.process.lock().await.kill()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
type RequestHandler = Box<
|
||||
dyn Send
|
||||
+ FnMut(
|
||||
u64,
|
||||
serde_json::Value,
|
||||
Arc<Mutex<async_pipe::PipeWriter>>,
|
||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
|
||||
>;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
type ResponseHandler = Box<dyn Send + Fn(Response)>;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeTransport {
|
||||
// for sending fake response back from adapter side
|
||||
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
|
||||
// for reverse request responses
|
||||
response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl FakeTransport {
|
||||
pub async fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F)
|
||||
where
|
||||
F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, ErrorResponse>,
|
||||
{
|
||||
self.request_handlers.lock().await.insert(
|
||||
R::COMMAND,
|
||||
Box::new(
|
||||
move |seq, args, writer: Arc<Mutex<async_pipe::PipeWriter>>| {
|
||||
let response = handler(seq, serde_json::from_value(args).unwrap());
|
||||
|
||||
let message = serde_json::to_string(&Message::Response(Response {
|
||||
seq: seq + 1,
|
||||
request_seq: seq,
|
||||
success: response.as_ref().is_ok(),
|
||||
command: R::COMMAND.into(),
|
||||
body: util::maybe!({ serde_json::to_value(response.ok()?).ok() }),
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
let writer = writer.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let mut writer = writer.lock().await;
|
||||
writer
|
||||
.write_all(TransportDelegate::build_rpc_message(message).as_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
})
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn on_response<R: dap_types::requests::Request, F>(&self, handler: F)
|
||||
where
|
||||
F: 'static + Send + Fn(Response),
|
||||
{
|
||||
self.response_handlers
|
||||
.lock()
|
||||
.await
|
||||
.insert(R::COMMAND, Box::new(handler));
|
||||
}
|
||||
|
||||
async fn start(cx: AsyncApp) -> Result<(TransportPipe, Self)> {
|
||||
let this = Self {
|
||||
request_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
response_handlers: Arc::new(Mutex::new(HashMap::default())),
|
||||
};
|
||||
use dap_types::requests::{Request, RunInTerminal, StartDebugging};
|
||||
use serde_json::json;
|
||||
|
||||
let (stdin_writer, stdin_reader) = async_pipe::pipe();
|
||||
let (stdout_writer, stdout_reader) = async_pipe::pipe();
|
||||
|
||||
let request_handlers = this.request_handlers.clone();
|
||||
let response_handlers = this.response_handlers.clone();
|
||||
let stdout_writer = Arc::new(Mutex::new(stdout_writer));
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let mut reader = BufReader::new(stdin_reader);
|
||||
let mut buffer = String::new();
|
||||
|
||||
loop {
|
||||
let message =
|
||||
TransportDelegate::receive_server_message(&mut reader, &mut buffer, None)
|
||||
.await;
|
||||
|
||||
match message {
|
||||
Err(error) => {
|
||||
break anyhow!(error);
|
||||
}
|
||||
Ok(message) => {
|
||||
match message {
|
||||
Message::Request(request) => {
|
||||
// redirect reverse requests to stdout writer/reader
|
||||
if request.command == RunInTerminal::COMMAND
|
||||
|| request.command == StartDebugging::COMMAND
|
||||
{
|
||||
let message =
|
||||
serde_json::to_string(&Message::Request(request))
|
||||
.unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
} else {
|
||||
if let Some(handle) = request_handlers
|
||||
.lock()
|
||||
.await
|
||||
.get_mut(request.command.as_str())
|
||||
{
|
||||
handle(
|
||||
request.seq,
|
||||
request.arguments.unwrap_or(json!({})),
|
||||
stdout_writer.clone(),
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
log::error!(
|
||||
"No request handler for {}",
|
||||
request.command
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Event(event) => {
|
||||
let message =
|
||||
serde_json::to_string(&Message::Event(event)).unwrap();
|
||||
|
||||
let mut writer = stdout_writer.lock().await;
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
Message::Response(response) => {
|
||||
if let Some(handle) = response_handlers
|
||||
.lock()
|
||||
.await
|
||||
.get(response.command.as_str())
|
||||
{
|
||||
handle(response);
|
||||
} else {
|
||||
log::error!("No response handler for {}", response.command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Ok((
|
||||
TransportPipe::new(Box::new(stdin_writer), Box::new(stdout_reader), None, None),
|
||||
this,
|
||||
))
|
||||
}
|
||||
|
||||
fn has_adapter_logs(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn kill(&self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
41
crates/dap_adapters/Cargo.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "dap_adapters"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"dap/test-support",
|
||||
"gpui/test-support",
|
||||
"task/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/dap_adapters.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
dap.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
paths.workspace = true
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
sysinfo.workspace = true
|
||||
task.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
task = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
1
crates/dap_adapters/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
79
crates/dap_adapters/src/custom.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use dap::transport::TcpTransport;
|
||||
use gpui::AsyncApp;
|
||||
use serde_json::Value;
|
||||
use std::{ffi::OsString, path::PathBuf};
|
||||
use task::DebugAdapterConfig;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct CustomDebugAdapter {
|
||||
custom_args: CustomArgs,
|
||||
}
|
||||
|
||||
impl CustomDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "custom_dap";
|
||||
|
||||
pub(crate) async fn new(custom_args: CustomArgs) -> Result<Self> {
|
||||
Ok(CustomDebugAdapter { custom_args })
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for CustomDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let connection = if let DebugConnectionType::TCP(connection) = &self.custom_args.connection
|
||||
{
|
||||
Some(adapters::TcpArguments {
|
||||
host: connection.host(),
|
||||
port: TcpTransport::port(&connection).await?,
|
||||
timeout: connection.timeout,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ret = DebugAdapterBinary {
|
||||
command: self.custom_args.command.clone(),
|
||||
arguments: self
|
||||
.custom_args
|
||||
.args
|
||||
.clone()
|
||||
.map(|args| args.iter().map(OsString::from).collect()),
|
||||
cwd: config.cwd.clone(),
|
||||
envs: self.custom_args.envs.clone(),
|
||||
connection,
|
||||
};
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
|
||||
bail!("Custom debug adapters don't have latest versions")
|
||||
}
|
||||
|
||||
async fn install_binary(&self, _: AdapterVersion, _: &dyn DapDelegate) -> Result<()> {
|
||||
bail!("Custom debug adapters cannot be installed")
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
_: &DebugAdapterConfig,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
bail!("Custom debug adapters cannot be installed")
|
||||
}
|
||||
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
||||
json!({"program": config.program})
|
||||
}
|
||||
}
|
||||
49
crates/dap_adapters/src/dap_adapters.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
mod custom;
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
mod gdb;
|
||||
mod go;
|
||||
mod javascript;
|
||||
mod lldb;
|
||||
mod php;
|
||||
mod python;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use async_trait::async_trait;
|
||||
use custom::CustomDebugAdapter;
|
||||
use dap::adapters::{
|
||||
self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName,
|
||||
GithubRepo,
|
||||
};
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
use gdb::GdbDebugAdapter;
|
||||
use go::GoDebugAdapter;
|
||||
use javascript::JsDebugAdapter;
|
||||
use lldb::LldbDebugAdapter;
|
||||
use php::PhpDebugAdapter;
|
||||
use python::PythonDebugAdapter;
|
||||
use serde_json::{json, Value};
|
||||
use task::{CustomArgs, DebugAdapterConfig, DebugAdapterKind, DebugConnectionType, TCPHost};
|
||||
|
||||
pub async fn build_adapter(kind: &DebugAdapterKind) -> Result<Arc<dyn DebugAdapter>> {
|
||||
match kind {
|
||||
DebugAdapterKind::Custom(start_args) => {
|
||||
Ok(Arc::new(CustomDebugAdapter::new(start_args.clone()).await?))
|
||||
}
|
||||
DebugAdapterKind::Python(host) => Ok(Arc::new(PythonDebugAdapter::new(host).await?)),
|
||||
DebugAdapterKind::Php(host) => Ok(Arc::new(PhpDebugAdapter::new(host.clone()).await?)),
|
||||
DebugAdapterKind::Javascript(host) => {
|
||||
Ok(Arc::new(JsDebugAdapter::new(host.clone()).await?))
|
||||
}
|
||||
DebugAdapterKind::Lldb => Ok(Arc::new(LldbDebugAdapter::new())),
|
||||
DebugAdapterKind::Go(host) => Ok(Arc::new(GoDebugAdapter::new(host).await?)),
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
DebugAdapterKind::Gdb => Ok(Arc::new(GdbDebugAdapter::new())),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
DebugAdapterKind::Fake => Ok(Arc::new(dap::adapters::FakeAdapter::new())),
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
#[allow(unreachable_patterns)]
|
||||
_ => unreachable!("Fake variant only exists with test-support feature"),
|
||||
}
|
||||
}
|
||||
83
crates/dap_adapters/src/gdb.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use std::ffi::OsStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::AsyncApp;
|
||||
use task::DebugAdapterConfig;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct GdbDebugAdapter {}
|
||||
|
||||
impl GdbDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "gdb";
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
GdbDebugAdapter {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for GdbDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
user_installed_path: Option<std::path::PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let user_setting_path = user_installed_path
|
||||
.filter(|p| p.exists())
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()));
|
||||
|
||||
/* GDB implements DAP natively so just need to */
|
||||
let gdb_path = delegate
|
||||
.which(OsStr::new("gdb"))
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.ok_or(anyhow!("Could not find gdb in path"));
|
||||
|
||||
if gdb_path.is_err() && user_setting_path.is_none() {
|
||||
bail!("Could not find gdb path or it's not installed");
|
||||
}
|
||||
|
||||
let gdb_path = user_setting_path.unwrap_or(gdb_path?);
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: gdb_path,
|
||||
arguments: Some(vec!["-i=dap".into()]),
|
||||
envs: None,
|
||||
cwd: config.cwd.clone(),
|
||||
connection: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
_version: AdapterVersion,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
unimplemented!("GDB debug adapter cannot be installed by Zed (yet)")
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
|
||||
unimplemented!("Fetch latest GDB version not implemented (yet)")
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
_: &DebugAdapterConfig,
|
||||
_: Option<std::path::PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
unimplemented!("GDB cannot be installed by Zed (yet)")
|
||||
}
|
||||
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
||||
json!({"program": config.program, "cwd": config.cwd})
|
||||
}
|
||||
}
|
||||
100
crates/dap_adapters/src/go.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use dap::transport::TcpTransport;
|
||||
use gpui::AsyncApp;
|
||||
use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct GoDebugAdapter {
|
||||
port: u16,
|
||||
host: Ipv4Addr,
|
||||
timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl GoDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "delve";
|
||||
|
||||
pub(crate) async fn new(host: &TCPHost) -> Result<Self> {
|
||||
Ok(GoDebugAdapter {
|
||||
port: TcpTransport::port(host).await?,
|
||||
host: host.host(),
|
||||
timeout: host.timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for GoDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
self.get_installed_binary(delegate, config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion> {
|
||||
unimplemented!("This adapter is used from path for now");
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Zip,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let delve_path = delegate
|
||||
.which(OsStr::new("dlv"))
|
||||
.and_then(|p| p.to_str().map(|p| p.to_string()))
|
||||
.ok_or(anyhow!("Dlv not found in path"))?;
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delve_path,
|
||||
arguments: Some(vec![
|
||||
"dap".into(),
|
||||
"--listen".into(),
|
||||
format!("{}:{}", self.host, self.port).into(),
|
||||
]),
|
||||
cwd: config.cwd.clone(),
|
||||
envs: None,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host: self.host,
|
||||
port: self.port,
|
||||
timeout: self.timeout,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
||||
json!({
|
||||
"program": config.program,
|
||||
"cwd": config.cwd,
|
||||
"subProcess": true,
|
||||
})
|
||||
}
|
||||
}
|
||||
153
crates/dap_adapters/src/javascript.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
use adapters::latest_github_release;
|
||||
use dap::transport::TcpTransport;
|
||||
use gpui::AsyncApp;
|
||||
use regex::Regex;
|
||||
use std::{collections::HashMap, net::Ipv4Addr, path::PathBuf};
|
||||
use sysinfo::{Pid, Process};
|
||||
use task::DebugRequestType;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct JsDebugAdapter {
|
||||
port: u16,
|
||||
host: Ipv4Addr,
|
||||
timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl JsDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "vscode-js-debug";
|
||||
const ADAPTER_PATH: &'static str = "js-debug/src/dapDebugServer.js";
|
||||
|
||||
pub(crate) async fn new(host: TCPHost) -> Result<Self> {
|
||||
Ok(JsDebugAdapter {
|
||||
host: host.host(),
|
||||
timeout: host.timeout,
|
||||
port: TcpTransport::port(&host).await?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for JsDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion> {
|
||||
let release = latest_github_release(
|
||||
&format!("{}/{}", "microsoft", Self::ADAPTER_NAME),
|
||||
true,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let asset_name = format!("js-debug-dap-{}.tar.gz", release.tag_name);
|
||||
|
||||
Ok(AdapterVersion {
|
||||
tag_name: release.tag_name,
|
||||
url: release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?
|
||||
.browser_download_url
|
||||
.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let adapter_path = if let Some(user_installed_path) = user_installed_path {
|
||||
user_installed_path
|
||||
} else {
|
||||
let adapter_path = paths::debug_adapters_dir().join(self.name());
|
||||
|
||||
let file_name_prefix = format!("{}_", self.name());
|
||||
|
||||
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
|
||||
file_name.starts_with(&file_name_prefix)
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))?
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
arguments: Some(vec![
|
||||
adapter_path.join(Self::ADAPTER_PATH).into(),
|
||||
self.port.to_string().into(),
|
||||
self.host.to_string().into(),
|
||||
]),
|
||||
cwd: config.cwd.clone(),
|
||||
envs: None,
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host: self.host,
|
||||
port: self.port,
|
||||
timeout: self.timeout,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::GzipTar,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
||||
let pid = if let DebugRequestType::Attach(attach_config) = &config.request {
|
||||
attach_config.process_id
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
json!({
|
||||
"program": config.program,
|
||||
"type": "pwa-node",
|
||||
"request": match config.request {
|
||||
DebugRequestType::Launch => "launch",
|
||||
DebugRequestType::Attach(_) => "attach",
|
||||
},
|
||||
"processId": pid,
|
||||
"cwd": config.cwd,
|
||||
})
|
||||
}
|
||||
|
||||
fn attach_processes<'a>(
|
||||
&self,
|
||||
processes: &'a HashMap<Pid, Process>,
|
||||
) -> Option<Vec<(&'a Pid, &'a Process)>> {
|
||||
let regex = Regex::new(r"(?i)^(?:node|bun|iojs)(?:$|\b)").unwrap();
|
||||
|
||||
Some(
|
||||
processes
|
||||
.iter()
|
||||
.filter(|(_, process)| regex.is_match(&process.name().to_string_lossy()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
107
crates/dap_adapters/src/lldb.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::AsyncApp;
|
||||
use sysinfo::{Pid, Process};
|
||||
use task::{DebugAdapterConfig, DebugRequestType};
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct LldbDebugAdapter {}
|
||||
|
||||
impl LldbDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "lldb";
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
LldbDebugAdapter {}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for LldbDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let lldb_dap_path = if let Some(user_installed_path) = user_installed_path {
|
||||
user_installed_path.to_string_lossy().into()
|
||||
} else if cfg!(target_os = "macos") {
|
||||
util::command::new_smol_command("xcrun")
|
||||
.args(&["-f", "lldb-dap"])
|
||||
.output()
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|output| String::from_utf8(output.stdout).ok())
|
||||
.map(|path| path.trim().to_string())
|
||||
.ok_or(anyhow!("Failed to find lldb-dap in user's path"))?
|
||||
} else {
|
||||
delegate
|
||||
.which(OsStr::new("lldb-dap"))
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.ok_or(anyhow!("Could not find lldb-dap in path"))?
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: lldb_dap_path,
|
||||
arguments: None,
|
||||
envs: None,
|
||||
cwd: config.cwd.clone(),
|
||||
connection: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
_version: AdapterVersion,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)")
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
|
||||
unimplemented!("Fetch latest adapter version not implemented for lldb (yet)")
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
_: &DebugAdapterConfig,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
unimplemented!("LLDB debug adapter cannot be installed by Zed (yet)")
|
||||
}
|
||||
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
||||
let pid = if let DebugRequestType::Attach(attach_config) = &config.request {
|
||||
attach_config.process_id
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
json!({
|
||||
"program": config.program,
|
||||
"request": match config.request {
|
||||
DebugRequestType::Launch => "launch",
|
||||
DebugRequestType::Attach(_) => "attach",
|
||||
},
|
||||
"pid": pid,
|
||||
"cwd": config.cwd,
|
||||
})
|
||||
}
|
||||
|
||||
fn attach_processes<'a>(
|
||||
&self,
|
||||
processes: &'a HashMap<Pid, Process>,
|
||||
) -> Option<Vec<(&'a Pid, &'a Process)>> {
|
||||
Some(processes.iter().collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
123
crates/dap_adapters/src/php.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use adapters::latest_github_release;
|
||||
use dap::{adapters::TcpArguments, transport::TcpTransport};
|
||||
use gpui::AsyncApp;
|
||||
use std::{net::Ipv4Addr, path::PathBuf};
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct PhpDebugAdapter {
|
||||
port: u16,
|
||||
_host: Ipv4Addr,
|
||||
_timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl PhpDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "vscode-php-debug";
|
||||
const ADAPTER_PATH: &'static str = "extension/out/phpDebug.js";
|
||||
|
||||
pub(crate) async fn new(host: TCPHost) -> Result<Self> {
|
||||
Ok(PhpDebugAdapter {
|
||||
port: TcpTransport::port(&host).await?,
|
||||
_host: host.host(),
|
||||
_timeout: host.timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for PhpDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion> {
|
||||
let release = latest_github_release(
|
||||
&format!("{}/{}", "xdebug", Self::ADAPTER_NAME),
|
||||
true,
|
||||
false,
|
||||
delegate.http_client(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let asset_name = format!("php-debug-{}.vsix", release.tag_name.replace("v", ""));
|
||||
|
||||
Ok(AdapterVersion {
|
||||
tag_name: release.tag_name,
|
||||
url: release
|
||||
.assets
|
||||
.iter()
|
||||
.find(|asset| asset.name == asset_name)
|
||||
.ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?
|
||||
.browser_download_url
|
||||
.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let adapter_path = if let Some(user_installed_path) = user_installed_path {
|
||||
user_installed_path
|
||||
} else {
|
||||
let adapter_path = paths::debug_adapters_dir().join(self.name());
|
||||
|
||||
let file_name_prefix = format!("{}_", self.name());
|
||||
|
||||
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
|
||||
file_name.starts_with(&file_name_prefix)
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))?
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: delegate
|
||||
.node_runtime()
|
||||
.binary_path()
|
||||
.await?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
arguments: Some(vec![
|
||||
adapter_path.join(Self::ADAPTER_PATH).into(),
|
||||
format!("--server={}", self.port).into(),
|
||||
]),
|
||||
connection: Some(TcpArguments {
|
||||
port: self.port,
|
||||
host: self._host,
|
||||
timeout: self._timeout,
|
||||
}),
|
||||
cwd: config.cwd.clone(),
|
||||
envs: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Vsix,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
||||
json!({
|
||||
"program": config.program,
|
||||
"cwd": config.cwd,
|
||||
})
|
||||
}
|
||||
}
|
||||
141
crates/dap_adapters/src/python.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use crate::*;
|
||||
use dap::transport::TcpTransport;
|
||||
use gpui::AsyncApp;
|
||||
use std::{ffi::OsStr, net::Ipv4Addr, path::PathBuf};
|
||||
|
||||
pub(crate) struct PythonDebugAdapter {
|
||||
port: u16,
|
||||
host: Ipv4Addr,
|
||||
timeout: Option<u64>,
|
||||
}
|
||||
|
||||
impl PythonDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "debugpy";
|
||||
const ADAPTER_PATH: &'static str = "src/debugpy/adapter";
|
||||
const LANGUAGE_NAME: &'static str = "Python";
|
||||
|
||||
pub(crate) async fn new(host: &TCPHost) -> Result<Self> {
|
||||
Ok(PythonDebugAdapter {
|
||||
port: TcpTransport::port(host).await?,
|
||||
host: host.host(),
|
||||
timeout: host.timeout,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for PythonDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion> {
|
||||
let github_repo = GithubRepo {
|
||||
repo_name: Self::ADAPTER_NAME.into(),
|
||||
repo_owner: "microsoft".into(),
|
||||
};
|
||||
|
||||
adapters::fetch_latest_adapter_version_from_github(github_repo, delegate).await
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
let version_path = adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Zip,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// only needed when you install the latest version for the first time
|
||||
if let Some(debugpy_dir) =
|
||||
util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| {
|
||||
file_name.starts_with("microsoft-debugpy-")
|
||||
})
|
||||
.await
|
||||
{
|
||||
// TODO Debugger: Rename folder instead of moving all files to another folder
|
||||
// We're doing uncessary IO work right now
|
||||
util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path())
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugAdapterConfig,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
|
||||
|
||||
let debugpy_dir = if let Some(user_installed_path) = user_installed_path {
|
||||
user_installed_path
|
||||
} else {
|
||||
let adapter_path = paths::debug_adapters_dir().join(self.name());
|
||||
let file_name_prefix = format!("{}_", self.name());
|
||||
|
||||
util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| {
|
||||
file_name.starts_with(&file_name_prefix)
|
||||
})
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("Debugpy directory not found"))?
|
||||
};
|
||||
|
||||
let toolchain = delegate
|
||||
.toolchain_store()
|
||||
.active_toolchain(
|
||||
delegate.worktree_id(),
|
||||
language::LanguageName::new(Self::LANGUAGE_NAME),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let python_path = if let Some(toolchain) = toolchain {
|
||||
Some(toolchain.path.to_string())
|
||||
} else {
|
||||
BINARY_NAMES
|
||||
.iter()
|
||||
.filter_map(|cmd| {
|
||||
delegate
|
||||
.which(OsStr::new(cmd))
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
})
|
||||
.find(|_| true)
|
||||
};
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: python_path.ok_or(anyhow!("failed to find binary path for python"))?,
|
||||
arguments: Some(vec![
|
||||
debugpy_dir.join(Self::ADAPTER_PATH).into(),
|
||||
format!("--port={}", self.port).into(),
|
||||
format!("--host={}", self.host).into(),
|
||||
]),
|
||||
connection: Some(adapters::TcpArguments {
|
||||
host: self.host,
|
||||
port: self.port,
|
||||
timeout: self.timeout,
|
||||
}),
|
||||
cwd: config.cwd.clone(),
|
||||
envs: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn request_args(&self, config: &DebugAdapterConfig) -> Value {
|
||||
json!({
|
||||
"program": config.program,
|
||||
"subProcess": true,
|
||||
"cwd": config.cwd,
|
||||
})
|
||||
}
|
||||
}
|
||||
26
crates/debugger_tools/Cargo.toml
Normal file
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "debugger_tools"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/debugger_tools.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
dap.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
project.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/debugger_tools/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
834
crates/debugger_tools/src/dap_log.rs
Normal file
@@ -0,0 +1,834 @@
|
||||
use dap::{
|
||||
client::SessionId,
|
||||
debugger_settings::DebuggerSettings,
|
||||
transport::{IoKind, LogKind},
|
||||
};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use futures::{
|
||||
channel::mpsc::{unbounded, UnboundedSender},
|
||||
StreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, div, App, AppContext, Context, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
||||
};
|
||||
use project::{debugger::session::Session, search::SearchQuery, Project};
|
||||
use settings::Settings as _;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::maybe;
|
||||
use workspace::{
|
||||
item::Item,
|
||||
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
ui::{h_flex, Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu},
|
||||
ToolbarItemEvent, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
struct DapLogView {
|
||||
editor: Entity<Editor>,
|
||||
focus_handle: FocusHandle,
|
||||
log_store: Entity<LogStore>,
|
||||
editor_subscriptions: Vec<Subscription>,
|
||||
current_view: Option<(SessionId, LogKind)>,
|
||||
project: Entity<Project>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct LogStore {
|
||||
projects: HashMap<WeakEntity<Project>, ProjectState>,
|
||||
debug_clients: HashMap<SessionId, DebugAdapterState>,
|
||||
rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
|
||||
adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
||||
struct DebugAdapterState {
|
||||
log_messages: VecDeque<String>,
|
||||
rpc_messages: RpcMessages,
|
||||
}
|
||||
|
||||
struct RpcMessages {
|
||||
messages: VecDeque<String>,
|
||||
last_message_kind: Option<MessageKind>,
|
||||
}
|
||||
|
||||
impl RpcMessages {
|
||||
const MESSAGE_QUEUE_LIMIT: usize = 255;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
last_message_kind: None,
|
||||
messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const SEND: &str = "// Send";
|
||||
const RECEIVE: &str = "// Receive";
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum MessageKind {
|
||||
Send,
|
||||
Receive,
|
||||
}
|
||||
|
||||
impl MessageKind {
|
||||
fn label(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Send => SEND,
|
||||
Self::Receive => RECEIVE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DebugAdapterState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
log_messages: VecDeque::new(),
|
||||
rpc_messages: RpcMessages::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogStore {
|
||||
fn new(cx: &Context<Self>) -> Self {
|
||||
let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((client_id, io_kind, message)) = rpc_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.on_rpc_log(client_id, io_kind, &message, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.on_adapter_log(client_id, io_kind, &message, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Self {
|
||||
rpc_tx,
|
||||
adapter_log_tx,
|
||||
projects: HashMap::new(),
|
||||
debug_clients: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_rpc_log(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_debug_client_message(client_id, io_kind, message.to_string(), cx);
|
||||
}
|
||||
|
||||
fn on_adapter_log(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_debug_client_log(client_id, io_kind, message.to_string(), cx);
|
||||
}
|
||||
|
||||
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
|
||||
let weak_project = project.downgrade();
|
||||
self.projects.insert(
|
||||
project.downgrade(),
|
||||
ProjectState {
|
||||
_subscriptions: [
|
||||
cx.observe_release(project, move |this, _, _| {
|
||||
this.projects.remove(&weak_project);
|
||||
}),
|
||||
cx.subscribe(project, |this, project, event, cx| match event {
|
||||
project::Event::DebugClientStarted(client_id) => {
|
||||
let session = project
|
||||
.read(cx)
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.session_by_id(client_id);
|
||||
if let Some(session) = session {
|
||||
this.add_debug_client(*client_id, session, cx);
|
||||
}
|
||||
}
|
||||
project::Event::DebugClientShutdown(client_id) => {
|
||||
this.remove_debug_client(*client_id, cx);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}),
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
|
||||
self.debug_clients.get_mut(&id)
|
||||
}
|
||||
|
||||
fn add_debug_client_message(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let kind = match io_kind {
|
||||
IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
|
||||
IoKind::StdIn => MessageKind::Send,
|
||||
};
|
||||
|
||||
let rpc_messages = &mut debug_client_state.rpc_messages;
|
||||
if rpc_messages.last_message_kind != Some(kind) {
|
||||
Self::add_debug_client_entry(
|
||||
&mut rpc_messages.messages,
|
||||
id,
|
||||
kind.label().to_string(),
|
||||
LogKind::Rpc,
|
||||
cx,
|
||||
);
|
||||
rpc_messages.last_message_kind = Some(kind);
|
||||
}
|
||||
Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add_debug_client_log(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: String,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let message = match io_kind {
|
||||
IoKind::StdErr => {
|
||||
let mut message = message.clone();
|
||||
message.insert_str(0, "stderr: ");
|
||||
message
|
||||
}
|
||||
_ => message,
|
||||
};
|
||||
|
||||
Self::add_debug_client_entry(
|
||||
&mut debug_client_state.log_messages,
|
||||
id,
|
||||
message,
|
||||
LogKind::Adapter,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn add_debug_client_entry(
|
||||
log_lines: &mut VecDeque<String>,
|
||||
id: SessionId,
|
||||
message: String,
|
||||
kind: LogKind,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
|
||||
log_lines.pop_front();
|
||||
}
|
||||
|
||||
let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
|
||||
|
||||
let entry = if format_messages {
|
||||
maybe!({
|
||||
serde_json::to_string_pretty::<serde_json::Value>(
|
||||
&serde_json::from_str(&message).ok()?,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or(message)
|
||||
} else {
|
||||
message
|
||||
};
|
||||
log_lines.push_back(entry.clone());
|
||||
|
||||
cx.emit(Event::NewLogEntry { id, entry, kind });
|
||||
}
|
||||
|
||||
fn add_debug_client(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
client: Entity<Session>,
|
||||
cx: &App,
|
||||
) -> Option<&mut DebugAdapterState> {
|
||||
let client_state = self
|
||||
.debug_clients
|
||||
.entry(client_id)
|
||||
.or_insert_with(DebugAdapterState::new);
|
||||
|
||||
let io_tx = self.rpc_tx.clone();
|
||||
|
||||
let client = client.read(cx).adapter_client()?;
|
||||
client.add_log_handler(
|
||||
move |io_kind, message| {
|
||||
io_tx
|
||||
.unbounded_send((client_id, io_kind, message.to_string()))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Rpc,
|
||||
);
|
||||
|
||||
let log_io_tx = self.adapter_log_tx.clone();
|
||||
client.add_log_handler(
|
||||
move |io_kind, message| {
|
||||
log_io_tx
|
||||
.unbounded_send((client_id, io_kind, message.to_string()))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Adapter,
|
||||
);
|
||||
|
||||
Some(client_state)
|
||||
}
|
||||
|
||||
fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) {
|
||||
self.debug_clients.remove(&client_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
|
||||
Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages)
|
||||
}
|
||||
|
||||
fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
|
||||
Some(
|
||||
&mut self
|
||||
.debug_clients
|
||||
.get_mut(&client_id)?
|
||||
.rpc_messages
|
||||
.messages,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DapLogToolbarItemView {
|
||||
log_view: Option<Entity<DapLogView>>,
|
||||
}
|
||||
|
||||
impl DapLogToolbarItemView {
|
||||
pub fn new() -> Self {
|
||||
Self { log_view: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DapLogToolbarItemView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(log_view) = self.log_view.clone() else {
|
||||
return Empty.into_any_element();
|
||||
};
|
||||
|
||||
let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| {
|
||||
(
|
||||
log_view.menu_items(cx).unwrap_or_default(),
|
||||
log_view.current_view.map(|(client_id, _)| client_id),
|
||||
)
|
||||
});
|
||||
|
||||
let current_client = current_client_id.and_then(|current_client_id| {
|
||||
menu_rows
|
||||
.iter()
|
||||
.find(|row| row.client_id == current_client_id)
|
||||
});
|
||||
|
||||
let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.trigger(Button::new(
|
||||
"debug_client_menu_header",
|
||||
current_client
|
||||
.map(|sub_item| {
|
||||
Cow::Owned(format!(
|
||||
"{} ({}) - {}",
|
||||
sub_item.client_name,
|
||||
sub_item.client_id.0,
|
||||
match sub_item.selected_entry {
|
||||
LogKind::Adapter => ADAPTER_LOGS,
|
||||
LogKind::Rpc => RPC_MESSAGES,
|
||||
}
|
||||
))
|
||||
})
|
||||
.unwrap_or_else(|| "No adapter selected".into()),
|
||||
))
|
||||
.menu(move |mut window, cx| {
|
||||
let log_view = log_view.clone();
|
||||
let menu_rows = menu_rows.clone();
|
||||
ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
|
||||
for row in menu_rows.into_iter() {
|
||||
menu = menu.custom_row(move |_window, _cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.pl_2()
|
||||
.child(
|
||||
Label::new(
|
||||
format!("{}. {}", row.client_id.0, row.client_name,),
|
||||
)
|
||||
.color(workspace::ui::Color::Muted),
|
||||
)
|
||||
.into_any_element()
|
||||
});
|
||||
|
||||
if row.has_adapter_logs {
|
||||
menu = menu.custom_entry(
|
||||
move |_window, _cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.pl_4()
|
||||
.child(Label::new(ADAPTER_LOGS))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_log_messages_for_adapter(row.client_id, window, cx);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.custom_entry(
|
||||
move |_window, _cx| {
|
||||
div()
|
||||
.w_full()
|
||||
.pl_4()
|
||||
.child(Label::new(RPC_MESSAGES))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_rpc_trace_for_server(row.client_id, window, cx);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
});
|
||||
|
||||
h_flex()
|
||||
.size_full()
|
||||
.child(dap_menu)
|
||||
.child(
|
||||
div()
|
||||
.child(
|
||||
Button::new("clear_log_button", "Clear").on_click(cx.listener(
|
||||
|this, _, window, cx| {
|
||||
if let Some(log_view) = this.log_view.as_ref() {
|
||||
log_view.update(cx, |log_view, cx| {
|
||||
log_view.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(false);
|
||||
editor.clear(window, cx);
|
||||
editor.set_read_only(true);
|
||||
});
|
||||
})
|
||||
}
|
||||
},
|
||||
)),
|
||||
)
|
||||
.ml_2(),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for DapLogToolbarItemView {}
|
||||
|
||||
impl ToolbarItemView for DapLogToolbarItemView {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn workspace::item::ItemHandle>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
if let Some(item) = active_pane_item {
|
||||
if let Some(log_view) = item.downcast::<DapLogView>() {
|
||||
self.log_view = Some(log_view.clone());
|
||||
return workspace::ToolbarItemLocation::PrimaryLeft;
|
||||
}
|
||||
}
|
||||
self.log_view = None;
|
||||
|
||||
cx.notify();
|
||||
|
||||
workspace::ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
impl DapLogView {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
log_store: Entity<LogStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(String::new(), window, cx);
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
|
||||
Event::NewLogEntry { id, entry, kind } => {
|
||||
if log_view.current_view == Some((*id, *kind)) {
|
||||
log_view.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(false);
|
||||
let last_point = editor.buffer().read(cx).len(cx);
|
||||
editor.edit(
|
||||
vec![
|
||||
(last_point..last_point, entry.trim()),
|
||||
(last_point..last_point, "\n"),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
editor.set_read_only(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
editor,
|
||||
focus_handle,
|
||||
project,
|
||||
log_store,
|
||||
editor_subscriptions,
|
||||
current_view: None,
|
||||
_subscriptions: vec![events_subscriptions],
|
||||
}
|
||||
}
|
||||
|
||||
fn editor_for_logs(
|
||||
log_contents: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> (Entity<Editor>, Vec<Subscription>) {
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::multi_line(window, cx);
|
||||
editor.set_text(log_contents, window, cx);
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor
|
||||
});
|
||||
let editor_subscription = cx.subscribe(
|
||||
&editor,
|
||||
|_, _, event: &EditorEvent, cx: &mut Context<'_, DapLogView>| cx.emit(event.clone()),
|
||||
);
|
||||
let search_subscription = cx.subscribe(
|
||||
&editor,
|
||||
|_, _, event: &SearchEvent, cx: &mut Context<'_, DapLogView>| cx.emit(event.clone()),
|
||||
);
|
||||
(editor, vec![editor_subscription, search_subscription])
|
||||
}
|
||||
|
||||
fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> {
|
||||
let mut menu_items = self
|
||||
.project
|
||||
.read(cx)
|
||||
.dap_store()
|
||||
.read(cx)
|
||||
.sessions()
|
||||
.filter_map(|client| {
|
||||
let client = client.read(cx).adapter_client()?;
|
||||
Some(DapMenuItem {
|
||||
client_id: client.id(),
|
||||
client_name: "debygpy (hard coded)".into(), // todo(debugger) Fix this hard coded
|
||||
has_adapter_logs: client.has_adapter_logs(),
|
||||
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
menu_items.sort_by_key(|item| item.client_id.0);
|
||||
Some(menu_items)
|
||||
}
|
||||
|
||||
fn show_rpc_trace_for_server(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.rpc_messages_for_client(client_id)
|
||||
.map(|state| log_contents(&state))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((client_id, LogKind::Rpc));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("log buffer should be a singleton")
|
||||
.update(cx, |_, cx| {
|
||||
cx.spawn({
|
||||
let buffer = cx.entity();
|
||||
|_, mut cx| async move {
|
||||
let language = language.await.ok();
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
|
||||
self.editor = editor;
|
||||
self.editor_subscriptions = editor_subscriptions;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
cx.focus_self(window);
|
||||
}
|
||||
|
||||
fn show_log_messages_for_adapter(
|
||||
&mut self,
|
||||
client_id: SessionId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let message_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.log_messages_for_client(client_id)
|
||||
.map(|state| log_contents(&state))
|
||||
});
|
||||
if let Some(message_log) = message_log {
|
||||
self.current_view = Some((client_id, LogKind::Adapter));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
|
||||
editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("log buffer should be a singleton");
|
||||
|
||||
self.editor = editor;
|
||||
self.editor_subscriptions = editor_subscriptions;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
cx.focus_self(window);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_contents(lines: &VecDeque<String>) -> String {
|
||||
let (a, b) = lines.as_slices();
|
||||
let a = a.iter().map(move |v| v.as_ref());
|
||||
let b = b.iter().map(move |v| v.as_ref());
|
||||
a.chain(b).fold(String::new(), |mut acc, el| {
|
||||
acc.push_str(el);
|
||||
acc.push('\n');
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub(crate) struct DapMenuItem {
|
||||
pub client_id: SessionId,
|
||||
pub client_name: String,
|
||||
pub has_adapter_logs: bool,
|
||||
pub selected_entry: LogKind,
|
||||
}
|
||||
|
||||
const ADAPTER_LOGS: &str = "Adapter Logs";
|
||||
const RPC_MESSAGES: &str = "RPC Messages";
|
||||
|
||||
impl Render for DapLogView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.render(window, cx).into_any_element()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
actions!(debug, [OpenDebuggerAdapterLogs]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let log_store = cx.new(|cx| LogStore::new(cx));
|
||||
|
||||
cx.observe_new(move |workspace: &mut Workspace, window, cx| {
|
||||
let Some(_window) = window else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project();
|
||||
if project.read(cx).is_local() {
|
||||
log_store.update(cx, |store, cx| {
|
||||
store.add_project(project, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let log_store = log_store.clone();
|
||||
workspace.register_action(move |workspace, _: &OpenDebuggerAdapterLogs, window, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_local() {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(cx.new(|cx| {
|
||||
DapLogView::new(workspace.project().clone(), log_store.clone(), window, cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
impl Item for DapLogView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("DAP Logs".into())
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableItem for DapLogView {
|
||||
type Match = <Editor as SearchableItem>::Match;
|
||||
|
||||
fn clear_matches(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |e, cx| e.clear_matches(window, cx))
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
matches: &[Self::Match],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.update_matches(matches, window, cx))
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.query_suggestion(window, cx))
|
||||
}
|
||||
|
||||
fn activate_match(
|
||||
&mut self,
|
||||
index: usize,
|
||||
matches: &[Self::Match],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.activate_match(index, matches, window, cx))
|
||||
}
|
||||
|
||||
fn select_matches(
|
||||
&mut self,
|
||||
matches: &[Self::Match],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.select_matches(matches, window, cx))
|
||||
}
|
||||
|
||||
fn find_matches(
|
||||
&mut self,
|
||||
query: Arc<project::search::SearchQuery>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::Task<Vec<Self::Match>> {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.find_matches(query, window, cx))
|
||||
}
|
||||
|
||||
fn replace(
|
||||
&mut self,
|
||||
_: &Self::Match,
|
||||
_: &SearchQuery,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
// Since DAP Log is read-only, it doesn't make sense to support replace operation.
|
||||
}
|
||||
|
||||
fn supported_options(&self) -> workspace::searchable::SearchOptions {
|
||||
workspace::searchable::SearchOptions {
|
||||
case: true,
|
||||
word: true,
|
||||
regex: true,
|
||||
find_in_results: true,
|
||||
// DAP log is read-only.
|
||||
replacement: false,
|
||||
selection: false,
|
||||
}
|
||||
}
|
||||
fn active_match_index(
|
||||
&mut self,
|
||||
matches: &[Self::Match],
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
self.editor
|
||||
.update(cx, |e, cx| e.active_match_index(matches, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for DapLogView {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
NewLogEntry {
|
||||
id: SessionId,
|
||||
entry: String,
|
||||
kind: LogKind,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for LogStore {}
|
||||
impl EventEmitter<Event> for DapLogView {}
|
||||
impl EventEmitter<EditorEvent> for DapLogView {}
|
||||
impl EventEmitter<SearchEvent> for DapLogView {}
|
||||
8
crates/debugger_tools/src/debugger_tools.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
mod dap_log;
|
||||
pub use dap_log::*;
|
||||
|
||||
use gpui::App;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
dap_log::init(cx);
|
||||
}
|
||||
59
crates/debugger_ui/Cargo.toml
Normal file
@@ -0,0 +1,59 @@
|
||||
[package]
|
||||
name = "debugger_ui"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"dap/test-support",
|
||||
"editor/test-support",
|
||||
"gpui/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
dap.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
menu.workspace = true
|
||||
picker.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project.workspace = true
|
||||
rpc.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
sum_tree.workspace = true
|
||||
sysinfo.workspace = true
|
||||
task.workspace = true
|
||||
tasks_ui.workspace = true
|
||||
terminal_view.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
1
crates/debugger_ui/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
304
crates/debugger_ui/src/attach_modal.rs
Normal file
@@ -0,0 +1,304 @@
|
||||
use dap::client::SessionId;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::Subscription;
|
||||
use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::debugger::dap_store::DapStore;
|
||||
use std::sync::Arc;
|
||||
use sysinfo::System;
|
||||
use ui::{prelude::*, Context, Tooltip};
|
||||
use ui::{ListItem, ListItemSpacing};
|
||||
use workspace::ModalView;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct _Candidate {
|
||||
pid: u32,
|
||||
name: String,
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
pub(crate) struct _AttachModalDelegate {
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
session_id: SessionId,
|
||||
placeholder_text: Arc<str>,
|
||||
dap_store: Entity<DapStore>,
|
||||
client_id: SessionId,
|
||||
candidates: Option<Vec<_Candidate>>,
|
||||
}
|
||||
|
||||
impl _AttachModalDelegate {
|
||||
pub fn _new(session_id: SessionId, client_id: SessionId, dap_store: Entity<DapStore>) -> Self {
|
||||
Self {
|
||||
client_id,
|
||||
dap_store,
|
||||
session_id,
|
||||
candidates: None,
|
||||
selected_index: 0,
|
||||
matches: Vec::default(),
|
||||
placeholder_text: Arc::from("Select the process you want to attach the debugger to"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct _AttachModal {
|
||||
_subscription: Subscription,
|
||||
pub(crate) picker: Entity<Picker<_AttachModalDelegate>>,
|
||||
}
|
||||
|
||||
impl _AttachModal {
|
||||
pub fn _new(
|
||||
session_id: &SessionId,
|
||||
client_id: SessionId,
|
||||
dap_store: Entity<DapStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::uniform_list(
|
||||
_AttachModalDelegate::_new(*session_id, client_id, dap_store),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let _subscription = cx.subscribe(&picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
Self {
|
||||
picker,
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for _AttachModal {
|
||||
fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
v_flex()
|
||||
.key_context("AttachModal")
|
||||
.w(rems(34.))
|
||||
.child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for _AttachModal {}
|
||||
|
||||
impl Focusable for _AttachModal {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.picker.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for _AttachModal {}
|
||||
|
||||
impl PickerDelegate for _AttachModalDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
|
||||
self.placeholder_text.clone()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let Some(processes) = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
if let Some(processes) = this.delegate.candidates.clone() {
|
||||
processes
|
||||
} else {
|
||||
let Some(_client) = this.delegate.dap_store.update(cx, |store, cx| {
|
||||
store
|
||||
.session_by_id(&this.delegate.client_id)
|
||||
.and_then(|client| client.read(cx).adapter_client())
|
||||
}) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let _system = System::new_all();
|
||||
|
||||
todo!("client.adapter().attach_processes(&system.processes())");
|
||||
// let processes: Vec<(&sysinfo::Pid, &sysinfo::Process)> = vec![];
|
||||
|
||||
// let processes = processes
|
||||
// .into_iter()
|
||||
// .map(|(pid, process)| _Candidate {
|
||||
// pid: pid.as_u32(),
|
||||
// name: process.name().to_string_lossy().into_owned(),
|
||||
// command: process
|
||||
// .cmd()
|
||||
// .iter()
|
||||
// .map(|s| s.to_string_lossy().to_string())
|
||||
// .collect::<Vec<_>>(),
|
||||
// })
|
||||
// .collect::<Vec<_Candidate>>();
|
||||
|
||||
// let _ = this.delegate.candidates.insert(processes.clone());
|
||||
|
||||
// processes
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&processes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, candidate)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
format!(
|
||||
"{} {} {}",
|
||||
candidate.command.join(" "),
|
||||
candidate.pid,
|
||||
candidate.name
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&query,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
this.update(&mut cx, |this, _| {
|
||||
let delegate = &mut this.delegate;
|
||||
|
||||
delegate.matches = matches;
|
||||
delegate.candidates = Some(processes);
|
||||
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_index = 0;
|
||||
} else {
|
||||
delegate.selected_index =
|
||||
delegate.selected_index.min(delegate.matches.len() - 1);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let candidate = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
.and_then(|current_match| {
|
||||
let ix = current_match.candidate_id;
|
||||
self.candidates.as_ref().map(|candidates| &candidates[ix])
|
||||
});
|
||||
let Some(_candidate) = candidate else {
|
||||
return cx.emit(DismissEvent);
|
||||
};
|
||||
|
||||
unimplemented!(
|
||||
r#"self.dap_store.update(cx, |store, cx| {{
|
||||
store
|
||||
.attach(self.client_id, candidate.pid, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}})"#
|
||||
);
|
||||
|
||||
// cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.selected_index = 0;
|
||||
self.candidates.take();
|
||||
|
||||
self.dap_store.update(cx, |store, cx| {
|
||||
store.shutdown_session(&self.session_id, cx).detach();
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let candidates = self.candidates.as_ref()?;
|
||||
let hit = &self.matches[ix];
|
||||
let candidate = &candidates.get(hit.candidate_id)?;
|
||||
|
||||
Some(
|
||||
ListItem::new(SharedString::from(format!("process-entry-{ix}")))
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(
|
||||
v_flex()
|
||||
.items_start()
|
||||
.child(Label::new(format!("{} {}", candidate.name, candidate.pid)))
|
||||
.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("process-entry-{ix}-command")))
|
||||
.tooltip(Tooltip::text(
|
||||
candidate
|
||||
.command
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
))
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} {}",
|
||||
candidate.name,
|
||||
candidate
|
||||
.command
|
||||
.clone()
|
||||
.into_iter()
|
||||
.skip(1)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn procss_names(modal: &_AttachModal, cx: &mut Context<_AttachModal>) -> Vec<String> {
|
||||
modal.picker.update(cx, |picker, _| {
|
||||
picker
|
||||
.delegate
|
||||
.matches
|
||||
.iter()
|
||||
.map(|hit| hit.string.clone())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
398
crates/debugger_ui/src/debugger_panel.rs
Normal file
@@ -0,0 +1,398 @@
|
||||
use crate::session::DebugSession;
|
||||
use anyhow::Result;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use dap::{
|
||||
client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
|
||||
ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
|
||||
};
|
||||
use gpui::{
|
||||
actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use project::{
|
||||
debugger::dap_store::{self, DapStore},
|
||||
Project,
|
||||
};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use ui::prelude::*;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop,
|
||||
ToggleIgnoreBreakpoints, Workspace,
|
||||
};
|
||||
|
||||
pub enum DebugPanelEvent {
|
||||
Exited(SessionId),
|
||||
Terminated(SessionId),
|
||||
Stopped {
|
||||
client_id: SessionId,
|
||||
event: StoppedEvent,
|
||||
go_to_stack_frame: bool,
|
||||
},
|
||||
Thread((SessionId, ThreadEvent)),
|
||||
Continued((SessionId, ContinuedEvent)),
|
||||
Output((SessionId, OutputEvent)),
|
||||
Module((SessionId, ModuleEvent)),
|
||||
LoadedSource((SessionId, LoadedSourceEvent)),
|
||||
ClientShutdown(SessionId),
|
||||
CapabilitiesChanged(SessionId),
|
||||
}
|
||||
|
||||
actions!(debug_panel, [ToggleFocus]);
|
||||
pub struct DebugPanel {
|
||||
size: Pixels,
|
||||
pane: Entity<Pane>,
|
||||
project: WeakEntity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let project = workspace.project().clone();
|
||||
let dap_store = project.read(cx).dap_store();
|
||||
let weak_workspace = workspace.weak_handle();
|
||||
let pane = cx.new(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
workspace.project().clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
gpui::NoAction.boxed_clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(None);
|
||||
pane.set_can_navigate(true, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_window, _cx| true);
|
||||
pane.set_close_pane_if_empty(true, cx);
|
||||
pane.set_render_tab_bar_buttons(cx, {
|
||||
let project = project.clone();
|
||||
let weak_workspace = weak_workspace.clone();
|
||||
move |_, _, cx| {
|
||||
let project = project.clone();
|
||||
let weak_workspace = weak_workspace.clone();
|
||||
(
|
||||
None,
|
||||
Some(
|
||||
h_flex()
|
||||
.child(
|
||||
IconButton::new("new-debug-session", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(move |pane, _, window, cx| {
|
||||
pane.add_item(
|
||||
Box::new(DebugSession::inert(
|
||||
project.clone(),
|
||||
weak_workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
}
|
||||
});
|
||||
pane.add_item(
|
||||
Box::new(DebugSession::inert(
|
||||
project.clone(),
|
||||
weak_workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pane
|
||||
});
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.observe(&pane, |_, _, cx| cx.notify()),
|
||||
cx.subscribe_in(&pane, window, Self::handle_pane_event),
|
||||
cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
|
||||
];
|
||||
|
||||
let debug_panel = Self {
|
||||
pane,
|
||||
size: px(300.),
|
||||
_subscriptions,
|
||||
project: project.downgrade(),
|
||||
workspace: workspace.weak_handle(),
|
||||
};
|
||||
|
||||
debug_panel
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let debug_panel = DebugPanel::new(workspace, window, cx);
|
||||
|
||||
cx.observe(&debug_panel, |_, debug_panel, cx| {
|
||||
let (has_active_session, support_step_back) =
|
||||
debug_panel.update(cx, |this, cx| {
|
||||
this.active_debug_panel_item(cx)
|
||||
.map(|_item| (true, false))
|
||||
.unwrap_or((false, false))
|
||||
});
|
||||
|
||||
let filter = CommandPaletteFilter::global_mut(cx);
|
||||
let debugger_action_types = [
|
||||
TypeId::of::<Continue>(),
|
||||
TypeId::of::<StepOver>(),
|
||||
TypeId::of::<StepInto>(),
|
||||
TypeId::of::<StepOut>(),
|
||||
TypeId::of::<Stop>(),
|
||||
TypeId::of::<Disconnect>(),
|
||||
TypeId::of::<Pause>(),
|
||||
TypeId::of::<Restart>(),
|
||||
TypeId::of::<ToggleIgnoreBreakpoints>(),
|
||||
];
|
||||
|
||||
let step_back_action_type = [TypeId::of::<StepBack>()];
|
||||
|
||||
if has_active_session {
|
||||
filter.show_action_types(debugger_action_types.iter());
|
||||
|
||||
if support_step_back {
|
||||
filter.show_action_types(step_back_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
}
|
||||
} else {
|
||||
// show only the `debug: start`
|
||||
filter.hide_action_types(&debugger_action_types);
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
debug_panel
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn message_queue(&self) -> &HashMap<SessionId, VecDeque<OutputEvent>> {
|
||||
// &self.message_queue
|
||||
unimplemented!("Should chekc session for console messagse")
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn dap_store(&self) -> Entity<DapStore> {
|
||||
self.dap_store.clone()
|
||||
}
|
||||
|
||||
pub fn active_debug_panel_item(&self, cx: &Context<Self>) -> Option<Entity<DebugSession>> {
|
||||
self.pane
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|panel| panel.downcast::<DebugSession>())
|
||||
}
|
||||
|
||||
pub fn debug_panel_items_by_client(
|
||||
&self,
|
||||
client_id: &SessionId,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<Entity<DebugSession>> {
|
||||
self.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.filter_map(|item| item.downcast::<DebugSession>())
|
||||
.filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
|
||||
.map(|item| item.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn debug_panel_item_by_client(
|
||||
&self,
|
||||
client_id: SessionId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<DebugSession>> {
|
||||
self.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.filter_map(|item| item.downcast::<DebugSession>())
|
||||
.find(|item| {
|
||||
let item = item.read(cx);
|
||||
|
||||
item.session_id(cx) == Some(client_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_dap_store_event(
|
||||
&mut self,
|
||||
dap_store: &Entity<DapStore>,
|
||||
event: &dap_store::DapStoreEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
|
||||
let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
|
||||
return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event");
|
||||
};
|
||||
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return log::error!("Debug Panel out lived it's weak reference to Project");
|
||||
};
|
||||
|
||||
let session_item =
|
||||
DebugSession::running(project, self.workspace.clone(), session, window, cx);
|
||||
|
||||
self.pane.update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(session_item), true, true, None, window, cx);
|
||||
window.focus(&pane.focus_handle(cx));
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_pane_event(
|
||||
&mut self,
|
||||
_: &Entity<Pane>,
|
||||
event: &pane::Event,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
|
||||
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
|
||||
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
|
||||
pane::Event::AddItem { item } => {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
item.added_to_pane(workspace, self.pane.clone(), window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<project::Event> for DebugPanel {}
|
||||
|
||||
impl Focusable for DebugPanel {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.pane.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for DebugPanel {
|
||||
fn pane(&self) -> Option<Entity<Pane>> {
|
||||
Some(self.pane.clone())
|
||||
}
|
||||
|
||||
fn persistent_name() -> &'static str {
|
||||
"DebugPanel"
|
||||
}
|
||||
|
||||
fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
|
||||
DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
position == DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn set_position(
|
||||
&mut self,
|
||||
_position: DockPosition,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn size(&self, _window: &Window, _cx: &App) -> Pixels {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||
self.size = size.unwrap();
|
||||
}
|
||||
|
||||
fn remote_id() -> Option<proto::PanelId> {
|
||||
Some(proto::PanelId::DebugPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
|
||||
Some(IconName::Debug)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
|
||||
if DebuggerSettings::get_global(cx).button {
|
||||
Some("Debug Panel")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
9
|
||||
}
|
||||
fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if active && self.pane.read(cx).items_len() == 0 {
|
||||
let Some(project) = self.project.clone().upgrade() else {
|
||||
return;
|
||||
};
|
||||
// todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
|
||||
self.pane.update(cx, |this, cx| {
|
||||
this.add_item(
|
||||
Box::new(DebugSession::inert(
|
||||
project,
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DebugPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("DebugPanel")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.child(self.pane.clone())
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
52
crates/debugger_ui/src/lib.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use dap::debugger_settings::DebuggerSettings;
|
||||
use debugger_panel::{DebugPanel, ToggleFocus};
|
||||
use feature_flags::{Debugger, FeatureFlagViewExt};
|
||||
use gpui::App;
|
||||
use session::DebugSession;
|
||||
use settings::Settings;
|
||||
use workspace::{ShutdownDebugAdapters, Start, Workspace};
|
||||
|
||||
pub mod attach_modal;
|
||||
pub mod debugger_panel;
|
||||
pub mod session;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
DebuggerSettings::register(cx);
|
||||
workspace::FollowableViewRegistry::register::<DebugSession>(cx);
|
||||
|
||||
cx.observe_new(|_: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.when_flag_enabled::<Debugger>(window, |workspace, _, _| {
|
||||
workspace
|
||||
.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<DebugPanel>(window, cx);
|
||||
})
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
tasks_ui::toggle_modal(
|
||||
workspace,
|
||||
None,
|
||||
task::TaskModal::DebugModal,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.register_action(
|
||||
|workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |store, cx| {
|
||||
store.shutdown_sessions(cx).detach();
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
251
crates/debugger_ui/src/session.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
mod inert;
|
||||
mod running;
|
||||
mod starting;
|
||||
|
||||
use dap::client::SessionId;
|
||||
use gpui::{
|
||||
AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use inert::{InertEvent, InertState};
|
||||
use project::debugger::{dap_store::DapStore, session::Session};
|
||||
use project::worktree_store::WorktreeStore;
|
||||
use project::Project;
|
||||
use rpc::proto::{self, PeerId};
|
||||
use running::RunningState;
|
||||
use starting::{StartingEvent, StartingState};
|
||||
use ui::prelude::*;
|
||||
use workspace::{
|
||||
item::{self, Item},
|
||||
FollowableItem, ViewId, Workspace,
|
||||
};
|
||||
|
||||
enum DebugSessionState {
|
||||
Inert(Entity<InertState>),
|
||||
Starting(Entity<StartingState>),
|
||||
Running(Entity<running::RunningState>),
|
||||
}
|
||||
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
mode: DebugSessionState,
|
||||
dap_store: WeakEntity<DapStore>,
|
||||
worktree_store: WeakEntity<WorktreeStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_subscriptions: [Subscription; 1],
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum DebugPanelItemEvent {
|
||||
Close,
|
||||
Stopped { go_to_stack_frame: bool },
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum ThreadItem {
|
||||
Console,
|
||||
LoadedSource,
|
||||
Modules,
|
||||
Variables,
|
||||
}
|
||||
|
||||
impl DebugSession {
|
||||
pub(super) fn inert(
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let inert = cx.new(|cx| InertState::new(window, cx));
|
||||
|
||||
let project = project.read(cx);
|
||||
let dap_store = project.dap_store().downgrade();
|
||||
let worktree_store = project.worktree_store().downgrade();
|
||||
cx.new(|cx| {
|
||||
let _subscriptions = [cx.subscribe_in(&inert, window, Self::on_inert_event)];
|
||||
Self {
|
||||
remote_id: None,
|
||||
mode: DebugSessionState::Inert(inert),
|
||||
dap_store,
|
||||
worktree_store,
|
||||
workspace,
|
||||
_subscriptions,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn running(
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
session: Entity<Session>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let mode = DebugSessionState::Running(
|
||||
cx.new(|cx| RunningState::new(session.clone(), workspace.clone(), window, cx)),
|
||||
);
|
||||
|
||||
cx.new(|cx| Self {
|
||||
remote_id: None,
|
||||
mode,
|
||||
dap_store: project.read(cx).dap_store().downgrade(),
|
||||
worktree_store: project.read(cx).worktree_store().downgrade(),
|
||||
workspace,
|
||||
_subscriptions: [cx.subscribe(&project, |_, _, _, _| {})], // todo(debugger) We don't need this subscription
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn session_id(&self, cx: &App) -> Option<SessionId> {
|
||||
match &self.mode {
|
||||
DebugSessionState::Inert(_) => None,
|
||||
DebugSessionState::Starting(_entity) => unimplemented!(),
|
||||
DebugSessionState::Running(entity) => Some(entity.read(cx).session_id()),
|
||||
}
|
||||
}
|
||||
fn on_inert_event(
|
||||
&mut self,
|
||||
_: &Entity<InertState>,
|
||||
event: &InertEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) {
|
||||
let dap_store = self.dap_store.clone();
|
||||
let InertEvent::Spawned { config } = event;
|
||||
let config = config.clone();
|
||||
let worktree = self
|
||||
.worktree_store
|
||||
.update(cx, |this, _| this.worktrees().next())
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("worktree-less project");
|
||||
let Ok(task) = dap_store.update(cx, |store, cx| {
|
||||
store.new_session(config, &worktree, None, cx)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let starting = cx.new(|cx| StartingState::new(task, cx));
|
||||
|
||||
self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)];
|
||||
self.mode = DebugSessionState::Starting(starting);
|
||||
}
|
||||
|
||||
fn on_starting_event(
|
||||
&mut self,
|
||||
_: &Entity<StartingState>,
|
||||
event: &StartingEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, Self>,
|
||||
) {
|
||||
let StartingEvent::Finished(Ok(session)) = event else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mode =
|
||||
cx.new(|cx| RunningState::new(session.clone(), self.workspace.clone(), window, cx));
|
||||
|
||||
self.mode = DebugSessionState::Running(mode);
|
||||
}
|
||||
}
|
||||
impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
|
||||
|
||||
impl Focusable for DebugSession {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.mode {
|
||||
DebugSessionState::Inert(inert_state) => inert_state.focus_handle(cx),
|
||||
DebugSessionState::Starting(starting_state) => starting_state.focus_handle(cx),
|
||||
DebugSessionState::Running(running_state) => running_state.focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for DebugSession {
|
||||
type Event = DebugPanelItemEvent;
|
||||
fn tab_content(&self, _: item::TabContentParams, _: &Window, _: &App) -> AnyElement {
|
||||
let label = match &self.mode {
|
||||
DebugSessionState::Inert(_) => "New Session",
|
||||
DebugSessionState::Starting(_) => "Starting",
|
||||
DebugSessionState::Running(_) => "Running",
|
||||
};
|
||||
div().child(Label::new(label)).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl FollowableItem for DebugSession {
|
||||
fn remote_id(&self) -> Option<workspace::ViewId> {
|
||||
self.remote_id
|
||||
}
|
||||
|
||||
fn to_state_proto(&self, _window: &Window, _cx: &App) -> Option<proto::view::Variant> {
|
||||
None
|
||||
}
|
||||
|
||||
fn from_state_proto(
|
||||
_workspace: Entity<Workspace>,
|
||||
_remote_id: ViewId,
|
||||
_state: &mut Option<proto::view::Variant>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut App,
|
||||
) -> Option<gpui::Task<gpui::Result<Entity<Self>>>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn add_event_to_update_proto(
|
||||
&self,
|
||||
_event: &Self::Event,
|
||||
_update: &mut Option<proto::update_view::Variant>,
|
||||
_window: &Window,
|
||||
_cx: &App,
|
||||
) -> bool {
|
||||
// update.get_or_insert_with(|| proto::update_view::Variant::DebugPanel(Default::default()));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn apply_update_proto(
|
||||
&mut self,
|
||||
_project: &Entity<project::Project>,
|
||||
_message: proto::update_view::Variant,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> gpui::Task<gpui::Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn set_leader_peer_id(
|
||||
&mut self,
|
||||
_leader_peer_id: Option<PeerId>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn to_follow_event(_event: &Self::Event) -> Option<workspace::item::FollowEvent> {
|
||||
None
|
||||
}
|
||||
|
||||
fn dedup(&self, existing: &Self, _window: &Window, cx: &App) -> Option<workspace::item::Dedup> {
|
||||
if existing.session_id(cx) == self.session_id(cx) {
|
||||
Some(item::Dedup::KeepExisting)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn is_project_item(&self, _window: &Window, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DebugSession {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
match &self.mode {
|
||||
DebugSessionState::Inert(inert_state) => {
|
||||
inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
|
||||
}
|
||||
DebugSessionState::Starting(starting_state) => {
|
||||
starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
|
||||
}
|
||||
DebugSessionState::Running(running_state) => {
|
||||
running_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
165
crates/debugger_ui/src/session/inert.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use dap::{DebugAdapterConfig, DebugAdapterKind, DebugRequestType};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle};
|
||||
use settings::Settings as _;
|
||||
use task::TCPHost;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
h_flex, relative, v_flex, ActiveTheme as _, Button, ButtonCommon, ButtonStyle, Clickable,
|
||||
Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, Label, ParentElement,
|
||||
Render, SharedString, Styled, Window,
|
||||
};
|
||||
|
||||
pub(super) struct InertState {
|
||||
focus_handle: FocusHandle,
|
||||
selected_debugger: Option<SharedString>,
|
||||
program_editor: Entity<Editor>,
|
||||
cwd_editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl InertState {
|
||||
pub(super) fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
selected_debugger: None,
|
||||
program_editor: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
cwd_editor: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Focusable for InertState {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum InertEvent {
|
||||
Spawned { config: DebugAdapterConfig },
|
||||
}
|
||||
|
||||
impl EventEmitter<InertEvent> for InertState {}
|
||||
|
||||
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
|
||||
|
||||
impl Render for InertState {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<'_, Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
let weak = cx.weak_entity();
|
||||
v_flex()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.gap_1()
|
||||
.p_1()
|
||||
.child(
|
||||
h_flex().child(DropdownMenu::new(
|
||||
"dap-adapter-picker",
|
||||
self.selected_debugger
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
|
||||
.clone(),
|
||||
ContextMenu::build(window, cx, move |this, _, _| {
|
||||
let setter_for_name = |name: &'static str| {
|
||||
let weak = weak.clone();
|
||||
move |_: &mut Window, cx: &mut App| {
|
||||
let name = name;
|
||||
(&weak)
|
||||
.update(cx, move |this, _| {
|
||||
this.selected_debugger = Some(name.into());
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
this.entry("GDB", None, setter_for_name("GDB"))
|
||||
.entry("Delve", None, setter_for_name("Delve"))
|
||||
.entry("LLDB", None, setter_for_name("LLDB"))
|
||||
.entry("PHP", None, setter_for_name("PHP"))
|
||||
.entry("JavaScript", None, setter_for_name("JavaScript"))
|
||||
.entry("Debugpy", None, setter_for_name("Debugpy"))
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_4_5()
|
||||
.gap_2()
|
||||
.child(Label::new("Program path"))
|
||||
.child(Self::render_editor(&self.program_editor, cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Label::new("Working directory"))
|
||||
.child(Self::render_editor(&self.cwd_editor, cx)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("launch-dap", "Launch")
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
let program = this.program_editor.read(cx).text(cx);
|
||||
let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
|
||||
let kind = kind_for_label(this.selected_debugger.as_deref().unwrap_or_else(|| unimplemented!("Automatic selection of a debugger based on users project")));
|
||||
cx.emit(InertEvent::Spawned {
|
||||
config: DebugAdapterConfig {
|
||||
label: "hard coded".into(),
|
||||
kind,
|
||||
request: DebugRequestType::Launch,
|
||||
program: Some(program),
|
||||
cwd: Some(cwd),
|
||||
initialize_args: None,
|
||||
supports_attach: false,
|
||||
},
|
||||
});
|
||||
})),
|
||||
)
|
||||
.child(Button::new("attach-dap", "Attach").style(ButtonStyle::Filled)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn kind_for_label(label: &str) -> DebugAdapterKind {
|
||||
match label {
|
||||
"LLDB" => DebugAdapterKind::Lldb,
|
||||
"Debugpy" => DebugAdapterKind::Python(TCPHost::default()),
|
||||
"JavaScript" => DebugAdapterKind::Javascript(TCPHost::default()),
|
||||
"PHP" => DebugAdapterKind::Php(TCPHost::default()),
|
||||
"Delve" => DebugAdapterKind::Go(TCPHost::default()),
|
||||
_ => {
|
||||
unimplemented!()
|
||||
} // Maybe we should set a toast notification here
|
||||
}
|
||||
}
|
||||
impl InertState {
|
||||
fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
680
crates/debugger_ui/src/session/running.rs
Normal file
@@ -0,0 +1,680 @@
|
||||
mod console;
|
||||
mod loaded_source_list;
|
||||
mod module_list;
|
||||
mod stack_frame_list;
|
||||
mod variable_list;
|
||||
|
||||
use super::{DebugPanelItemEvent, ThreadItem};
|
||||
use console::Console;
|
||||
use dap::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities};
|
||||
use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity};
|
||||
use loaded_source_list::LoadedSourceList;
|
||||
use module_list::ModuleList;
|
||||
use project::debugger::session::{Session, ThreadId, ThreadStatus};
|
||||
use rpc::proto::ViewId;
|
||||
use settings::Settings;
|
||||
use stack_frame_list::{StackFrameList, StackFrameListEvent};
|
||||
use ui::{
|
||||
div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize,
|
||||
Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Tooltip, Window,
|
||||
};
|
||||
use variable_list::VariableList;
|
||||
use workspace::{item::ItemEvent, Item, Workspace};
|
||||
|
||||
pub struct RunningState {
|
||||
session: Entity<Session>,
|
||||
thread: Option<(ThreadId, String)>,
|
||||
console: Entity<console::Console>,
|
||||
focus_handle: FocusHandle,
|
||||
_remote_id: Option<ViewId>,
|
||||
show_console_indicator: bool,
|
||||
module_list: Entity<module_list::ModuleList>,
|
||||
active_thread_item: ThreadItem,
|
||||
_workspace: WeakEntity<Workspace>,
|
||||
session_id: SessionId,
|
||||
variable_list: Entity<variable_list::VariableList>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
stack_frame_list: Entity<stack_frame_list::StackFrameList>,
|
||||
loaded_source_list: Entity<loaded_source_list::LoadedSourceList>,
|
||||
}
|
||||
|
||||
impl Render for RunningState {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let threads = self.session.update(cx, |this, cx| this.threads(cx));
|
||||
if let Some((thread, _)) = threads.first().filter(|_| self.thread.is_none()) {
|
||||
self.select_thread(ThreadId(thread.id), thread.name.clone(), cx);
|
||||
}
|
||||
|
||||
let thread_status = self
|
||||
.thread
|
||||
.as_ref()
|
||||
.map(|(thread_id, _)| self.session.read(cx).thread_status(*thread_id))
|
||||
.unwrap_or(ThreadStatus::Exited);
|
||||
let is_terminated = self.session.read(cx).is_terminated();
|
||||
let active_thread_item = &self.active_thread_item;
|
||||
|
||||
let has_no_threads = threads.is_empty();
|
||||
let capabilities = self.capabilities(cx);
|
||||
let state = cx.entity();
|
||||
h_flex()
|
||||
.when(is_terminated, |this| this.bg(gpui::red()))
|
||||
.key_context("DebugPanelItem")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.items_start()
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if thread_status == ThreadStatus::Running {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"debug-pause",
|
||||
IconName::DebugPause,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.pause_thread(cx);
|
||||
}))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Pause program")(window, cx)
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"debug-continue",
|
||||
IconName::DebugContinue,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.continue_thread(cx)
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Continue program")(window, cx)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
.when(
|
||||
capabilities.supports_step_back.unwrap_or(false),
|
||||
|this| {
|
||||
this.child(
|
||||
IconButton::new(
|
||||
"debug-step-back",
|
||||
IconName::DebugStepBack,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_back(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step back")(window, cx)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-over", IconName::DebugStepOver)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_over(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step over")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-in", IconName::DebugStepInto)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_in(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step in")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-out", IconName::DebugStepOut)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.step_out(cx);
|
||||
}))
|
||||
.disabled(thread_status != ThreadStatus::Stopped)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Step out")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-restart", IconName::DebugRestart)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.restart_client(cx);
|
||||
}))
|
||||
.disabled(
|
||||
!capabilities
|
||||
.supports_restart_request
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Restart")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-stop", IconName::DebugStop)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.stop_thread(cx);
|
||||
}))
|
||||
.disabled(
|
||||
thread_status != ThreadStatus::Stopped
|
||||
&& thread_status != ThreadStatus::Running,
|
||||
)
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Stop")(window, cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"debug-disconnect",
|
||||
IconName::DebugDisconnect,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.disconnect_client(cx);
|
||||
}))
|
||||
.disabled(
|
||||
thread_status == ThreadStatus::Exited
|
||||
|| thread_status == ThreadStatus::Ended,
|
||||
)
|
||||
.tooltip(
|
||||
move |window, cx| {
|
||||
Tooltip::text("Disconnect")(window, cx)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"debug-ignore-breakpoints",
|
||||
if self.session.read(cx).breakpoints_enabled() {
|
||||
IconName::DebugBreakpoint
|
||||
} else {
|
||||
IconName::DebugIgnoreBreakpoints
|
||||
},
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
this.toggle_ignore_breakpoints(cx);
|
||||
}))
|
||||
.disabled(
|
||||
thread_status == ThreadStatus::Exited
|
||||
|| thread_status == ThreadStatus::Ended,
|
||||
)
|
||||
.tooltip(
|
||||
move |window, cx| {
|
||||
Tooltip::text("Ignore breakpoints")(window, cx)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
//.child(h_flex())
|
||||
.child(
|
||||
h_flex().p_1().mx_2().w_3_4().justify_end().child(
|
||||
DropdownMenu::new(
|
||||
("thread-list", self.session_id.0),
|
||||
self.thread
|
||||
.as_ref()
|
||||
.map(|(_, name)| format!("Thread {name}"))
|
||||
.unwrap_or_else(|| "Threads".into()),
|
||||
ContextMenu::build(window, cx, move |mut this, _, _| {
|
||||
for (thread, _) in threads {
|
||||
let state = state.clone();
|
||||
let thread_id = thread.id;
|
||||
let thread_name = SharedString::from(&thread.name);
|
||||
this =
|
||||
this.entry(thread.name, None, move |_, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.select_thread(
|
||||
ThreadId(thread_id),
|
||||
String::from(thread_name.as_ref()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
this
|
||||
}),
|
||||
)
|
||||
.disabled(has_no_threads),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size_full()
|
||||
.items_start()
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.child(self.stack_frame_list.clone()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.size_full()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.border_b_1()
|
||||
.w_full()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(self.render_entry_button(
|
||||
&SharedString::from("Variables"),
|
||||
ThreadItem::Variables,
|
||||
cx,
|
||||
))
|
||||
.when(
|
||||
capabilities.supports_modules_request.unwrap_or_default(),
|
||||
|this| {
|
||||
this.child(self.render_entry_button(
|
||||
&SharedString::from("Modules"),
|
||||
ThreadItem::Modules,
|
||||
cx,
|
||||
))
|
||||
},
|
||||
)
|
||||
.when(
|
||||
capabilities
|
||||
.supports_loaded_sources_request
|
||||
.unwrap_or_default(),
|
||||
|this| {
|
||||
this.child(self.render_entry_button(
|
||||
&SharedString::from("Loaded Sources"),
|
||||
ThreadItem::LoadedSource,
|
||||
cx,
|
||||
))
|
||||
},
|
||||
)
|
||||
.child(self.render_entry_button(
|
||||
&SharedString::from("Console"),
|
||||
ThreadItem::Console,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(*active_thread_item == ThreadItem::Variables, |this| {
|
||||
this.size_full().child(self.variable_list.clone())
|
||||
})
|
||||
.when(*active_thread_item == ThreadItem::Modules, |this| {
|
||||
this.size_full().child(self.module_list.clone())
|
||||
})
|
||||
.when(*active_thread_item == ThreadItem::LoadedSource, |this| {
|
||||
this.size_full().child(self.loaded_source_list.clone())
|
||||
})
|
||||
.when(*active_thread_item == ThreadItem::Console, |this| {
|
||||
this.child(self.console.clone())
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl RunningState {
|
||||
pub fn new(
|
||||
session: Entity<Session>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let session_id = session.read(cx).session_id();
|
||||
let stack_frame_list =
|
||||
cx.new(|cx| StackFrameList::new(workspace.clone(), session.clone(), cx));
|
||||
|
||||
let variable_list =
|
||||
cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
|
||||
|
||||
let module_list = cx.new(|cx| ModuleList::new(session.clone(), cx));
|
||||
|
||||
let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
|
||||
|
||||
let console = cx.new(|cx| {
|
||||
Console::new(
|
||||
session.clone(),
|
||||
stack_frame_list.clone(),
|
||||
variable_list.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
cx.observe(&module_list, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
let _subscriptions = vec![cx.subscribe(
|
||||
&stack_frame_list,
|
||||
move |this: &mut Self, _, event: &StackFrameListEvent, cx| match event {
|
||||
StackFrameListEvent::SelectedStackFrameChanged(_) => this.clear_highlights(cx),
|
||||
},
|
||||
)];
|
||||
|
||||
Self {
|
||||
session,
|
||||
console,
|
||||
_workspace: workspace,
|
||||
module_list,
|
||||
focus_handle,
|
||||
variable_list,
|
||||
_subscriptions,
|
||||
thread: None,
|
||||
_remote_id: None,
|
||||
stack_frame_list,
|
||||
loaded_source_list,
|
||||
session_id,
|
||||
show_console_indicator: false,
|
||||
active_thread_item: ThreadItem::Variables,
|
||||
}
|
||||
}
|
||||
|
||||
// pub(crate) fn update_adapter(
|
||||
// &mut self,
|
||||
// update: &UpdateDebugAdapter,
|
||||
// window: &mut Window,
|
||||
// cx: &mut Context<Self>,
|
||||
// ) {
|
||||
// if let Some(update_variant) = update.variant.as_ref() {
|
||||
// match update_variant {
|
||||
// proto::update_debug_adapter::Variant::StackFrameList(stack_frame_list) => {
|
||||
// self.stack_frame_list.update(cx, |this, cx| {
|
||||
// this.set_from_proto(stack_frame_list.clone(), cx);
|
||||
// })
|
||||
// }
|
||||
// proto::update_debug_adapter::Variant::ThreadState(thread_state) => {
|
||||
// self.thread_state.update(cx, |this, _| {
|
||||
// *this = ThreadState::from_proto(thread_state.clone());
|
||||
// })
|
||||
// }
|
||||
// proto::update_debug_adapter::Variant::VariableList(variable_list) => self
|
||||
// .variable_list
|
||||
// .update(cx, |this, cx| this.set_from_proto(variable_list, cx)),
|
||||
// proto::update_debug_adapter::Variant::AddToVariableList(variables_to_add) => self
|
||||
// .variable_list
|
||||
// .update(cx, |this, _| this.add_variables(variables_to_add.clone())),
|
||||
// proto::update_debug_adapter::Variant::Modules(_) => {}
|
||||
// proto::update_debug_adapter::Variant::OutputEvent(output_event) => {
|
||||
// self.console.update(cx, |this, cx| {
|
||||
// this.add_message(OutputEvent::from_proto(output_event.clone()), window, cx);
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn session(&self) -> &Entity<Session> {
|
||||
&self.session
|
||||
}
|
||||
|
||||
pub fn session_id(&self) -> SessionId {
|
||||
self.session_id
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
|
||||
self.active_thread_item = thread_item;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
|
||||
&self.stack_frame_list
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn console(&self) -> &Entity<Console> {
|
||||
&self.console
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn module_list(&self) -> &Entity<ModuleList> {
|
||||
&self.module_list
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn variable_list(&self) -> &Entity<VariableList> {
|
||||
&self.variable_list
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn are_breakpoints_ignored(&self, cx: &App) -> bool {
|
||||
self.session.read(cx).ignore_breakpoints()
|
||||
}
|
||||
|
||||
pub fn capabilities(&self, cx: &mut Context<Self>) -> Capabilities {
|
||||
self.session().read(cx).capabilities().clone()
|
||||
}
|
||||
|
||||
fn select_thread(&mut self, thread_id: ThreadId, thread_name: String, cx: &mut Context<Self>) {
|
||||
self.thread = Some((thread_id, thread_name));
|
||||
|
||||
self.stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.set_thread_id(self.thread.as_ref().map(|id| id.0), cx);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn clear_highlights(&self, _cx: &mut Context<Self>) {
|
||||
// TODO(debugger): make this work again
|
||||
// if let Some((_, project_path, _)) = self.dap_store.read(cx).active_debug_line() {
|
||||
// self.workspace
|
||||
// .update(cx, |workspace, cx| {
|
||||
// let editor = workspace
|
||||
// .items_of_type::<Editor>(cx)
|
||||
// .find(|editor| Some(project_path.clone()) == editor.project_path(cx));
|
||||
|
||||
// if let Some(editor) = editor {
|
||||
// editor.update(cx, |editor, cx| {
|
||||
// editor.clear_row_highlights::<editor::DebugCurrentRowHighlight>();
|
||||
|
||||
// cx.notify();
|
||||
// });
|
||||
// }
|
||||
// })
|
||||
// .ok();
|
||||
// }
|
||||
}
|
||||
|
||||
pub fn _go_to_current_stack_frame(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
if let Some(stack_frame) = stack_frame_list
|
||||
.stack_frames(cx)
|
||||
.iter()
|
||||
.find(|frame| frame.dap.id == stack_frame_list.current_stack_frame_id())
|
||||
.cloned()
|
||||
{
|
||||
stack_frame_list
|
||||
.select_stack_frame(&stack_frame.dap, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn render_entry_button(
|
||||
&self,
|
||||
label: &SharedString,
|
||||
thread_item: ThreadItem,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let has_indicator =
|
||||
matches!(thread_item, ThreadItem::Console) && self.show_console_indicator;
|
||||
|
||||
div()
|
||||
.id(label.clone())
|
||||
.px_2()
|
||||
.py_1()
|
||||
.cursor_pointer()
|
||||
.border_b_2()
|
||||
.when(self.active_thread_item == thread_item, |this| {
|
||||
this.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.child(Button::new(label.clone(), label.clone()))
|
||||
.when(has_indicator, |this| this.child(Indicator::dot())),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.active_thread_item = thread_item.clone();
|
||||
|
||||
if matches!(this.active_thread_item, ThreadItem::Console) {
|
||||
this.show_console_indicator = false;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
|
||||
let Some((thread_id, _)) = self.thread else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.continue_thread(thread_id, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn step_over(&mut self, cx: &mut Context<Self>) {
|
||||
let Some((thread_id, _)) = self.thread else {
|
||||
return;
|
||||
};
|
||||
|
||||
let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
|
||||
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.step_over(thread_id, granularity, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn step_in(&mut self, cx: &mut Context<Self>) {
|
||||
let Some((thread_id, _)) = self.thread else {
|
||||
return;
|
||||
};
|
||||
|
||||
let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
|
||||
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.step_in(thread_id, granularity, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn step_out(&mut self, cx: &mut Context<Self>) {
|
||||
let Some((thread_id, _)) = self.thread else {
|
||||
return;
|
||||
};
|
||||
|
||||
let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
|
||||
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.step_out(thread_id, granularity, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn step_back(&mut self, cx: &mut Context<Self>) {
|
||||
let Some((thread_id, _)) = self.thread else {
|
||||
return;
|
||||
};
|
||||
|
||||
let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
|
||||
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.step_back(thread_id, granularity, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn restart_client(&self, cx: &mut Context<Self>) {
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.restart(None, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pause_thread(&self, cx: &mut Context<Self>) {
|
||||
let Some((thread_id, _)) = self.thread else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.pause_thread(thread_id, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn stop_thread(&self, cx: &mut Context<Self>) {
|
||||
let Some((thread_id, _)) = self.thread else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.terminate_threads(Some(vec![thread_id; 1]), cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn disconnect_client(&self, cx: &mut Context<Self>) {
|
||||
self.session().update(cx, |state, cx| {
|
||||
state.disconnect_client(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
|
||||
self.session
|
||||
.update(cx, |session, cx| session.toggle_ignore_breakpoints(cx))
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DebugPanelItemEvent> for RunningState {}
|
||||
|
||||
impl Focusable for RunningState {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for RunningState {
|
||||
type Event = DebugPanelItemEvent;
|
||||
|
||||
fn tab_content(
|
||||
&self,
|
||||
_params: workspace::item::TabContentParams,
|
||||
_window: &Window,
|
||||
_cx: &App,
|
||||
) -> AnyElement {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _cx: &App) -> Option<SharedString> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
||||
match event {
|
||||
DebugPanelItemEvent::Close => f(ItemEvent::CloseItem),
|
||||
DebugPanelItemEvent::Stopped { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
505
crates/debugger_ui/src/session/running/console.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
use super::{
|
||||
stack_frame_list::{StackFrameList, StackFrameListEvent},
|
||||
variable_list::VariableList,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use dap::{OutputEvent, OutputEventGroup};
|
||||
use editor::{
|
||||
display_map::{Crease, CreaseId},
|
||||
Anchor, CompletionProvider, Editor, EditorElement, EditorStyle, FoldPlaceholder,
|
||||
};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{Context, Entity, Render, Subscription, Task, TextStyle, WeakEntity};
|
||||
use language::{Buffer, CodeLabel, LanguageServerId};
|
||||
use menu::Confirm;
|
||||
use project::{
|
||||
debugger::session::{CompletionsQuery, Session},
|
||||
Completion,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc, usize};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, ButtonLike, Disclosure, ElevationIndex};
|
||||
|
||||
pub struct OutputGroup {
|
||||
pub start: Anchor,
|
||||
pub collapsed: bool,
|
||||
pub end: Option<Anchor>,
|
||||
pub crease_ids: Vec<CreaseId>,
|
||||
pub placeholder: SharedString,
|
||||
}
|
||||
|
||||
pub struct Console {
|
||||
groups: Vec<OutputGroup>,
|
||||
console: Entity<Editor>,
|
||||
query_bar: Entity<Editor>,
|
||||
session: Entity<Session>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
variable_list: Entity<VariableList>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
}
|
||||
|
||||
impl Console {
|
||||
pub fn new(
|
||||
session: Entity<Session>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
variable_list: Entity<VariableList>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let console = cx.new(|cx| {
|
||||
let mut editor = Editor::multi_line(window, cx);
|
||||
editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
editor.set_read_only(true);
|
||||
editor.set_show_gutter(true, cx);
|
||||
editor.set_show_runnables(false, cx);
|
||||
editor.set_show_code_actions(false, cx);
|
||||
editor.set_show_line_numbers(false, cx);
|
||||
editor.set_show_git_diff_gutter(false, cx);
|
||||
editor.set_autoindent(false);
|
||||
editor.set_input_enabled(false);
|
||||
editor.set_use_autoclose(false);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_show_edit_predictions(Some(false), window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let this = cx.weak_entity();
|
||||
let query_bar = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Evaluate an expression", cx);
|
||||
editor.set_use_autoclose(false);
|
||||
editor.set_show_gutter(false, cx);
|
||||
editor.set_show_wrap_guides(false, cx);
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_completion_provider(Some(Box::new(ConsoleQueryBarCompletionProvider(this))));
|
||||
|
||||
editor
|
||||
});
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
|
||||
cx.observe_in(&session, window, |console, session, window, cx| {
|
||||
let (output, last_processed_ix) = session.update(cx, |session, _| {
|
||||
(session.output(), session.last_processed_output())
|
||||
});
|
||||
|
||||
if output.len() > last_processed_ix {
|
||||
for event in &output[last_processed_ix..] {
|
||||
console.add_message(event.clone(), window, cx);
|
||||
}
|
||||
|
||||
session.update(cx, |session, _| {
|
||||
session.set_last_processed_output(output.len());
|
||||
});
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
session,
|
||||
console,
|
||||
query_bar,
|
||||
variable_list,
|
||||
_subscriptions,
|
||||
stack_frame_list,
|
||||
groups: Vec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn editor(&self) -> &Entity<Editor> {
|
||||
&self.console
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn query_bar(&self) -> &Entity<Editor> {
|
||||
&self.query_bar
|
||||
}
|
||||
|
||||
fn is_local(&self, cx: &Context<Self>) -> bool {
|
||||
self.session.read(cx).is_local()
|
||||
}
|
||||
|
||||
fn handle_stack_frame_list_events(
|
||||
&mut self,
|
||||
_: Entity<StackFrameList>,
|
||||
event: &StackFrameListEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
StackFrameListEvent::SelectedStackFrameChanged(_) => cx.notify(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_message(&mut self, event: OutputEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.console.update(cx, |console, cx| {
|
||||
let output = event.output.trim_end().to_string();
|
||||
|
||||
let snapshot = console.buffer().read(cx).snapshot(cx);
|
||||
|
||||
let start = snapshot.anchor_before(snapshot.max_point());
|
||||
|
||||
let mut indent_size = self
|
||||
.groups
|
||||
.iter()
|
||||
.filter(|group| group.end.is_none())
|
||||
.count();
|
||||
if Some(OutputEventGroup::End) == event.group {
|
||||
indent_size = indent_size.saturating_sub(1);
|
||||
}
|
||||
|
||||
let indent = if indent_size > 0 {
|
||||
" ".repeat(indent_size)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
console.set_read_only(false);
|
||||
console.move_to_end(&editor::actions::MoveToEnd, window, cx);
|
||||
console.insert(format!("{}{}\n", indent, output).as_str(), window, cx);
|
||||
console.set_read_only(true);
|
||||
|
||||
let end = snapshot.anchor_before(snapshot.max_point());
|
||||
|
||||
match event.group {
|
||||
Some(OutputEventGroup::Start) => {
|
||||
self.groups.push(OutputGroup {
|
||||
start,
|
||||
end: None,
|
||||
collapsed: false,
|
||||
placeholder: output.clone().into(),
|
||||
crease_ids: console.insert_creases(
|
||||
vec![Self::create_crease(output.into(), start, end)],
|
||||
cx,
|
||||
),
|
||||
});
|
||||
}
|
||||
Some(OutputEventGroup::StartCollapsed) => {
|
||||
self.groups.push(OutputGroup {
|
||||
start,
|
||||
end: None,
|
||||
collapsed: true,
|
||||
placeholder: output.clone().into(),
|
||||
crease_ids: console.insert_creases(
|
||||
vec![Self::create_crease(output.into(), start, end)],
|
||||
cx,
|
||||
),
|
||||
});
|
||||
}
|
||||
Some(OutputEventGroup::End) => {
|
||||
if let Some(index) = self.groups.iter().rposition(|group| group.end.is_none()) {
|
||||
let group = self.groups.remove(index);
|
||||
|
||||
console.remove_creases(group.crease_ids.clone(), cx);
|
||||
|
||||
let creases =
|
||||
vec![Self::create_crease(group.placeholder, group.start, end)];
|
||||
console.insert_creases(creases.clone(), cx);
|
||||
|
||||
if group.collapsed {
|
||||
console.fold_creases(creases, false, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn create_crease(placeholder: SharedString, start: Anchor, end: Anchor) -> Crease<Anchor> {
|
||||
Crease::inline(
|
||||
start..end,
|
||||
FoldPlaceholder {
|
||||
render: Arc::new({
|
||||
let placeholder = placeholder.clone();
|
||||
move |_id, _range, _cx| {
|
||||
ButtonLike::new("output-group-placeholder")
|
||||
.style(ButtonStyle::Transparent)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.child(Label::new(placeholder.clone()).single_line())
|
||||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
move |row, is_folded, fold, _window, _cx| {
|
||||
Disclosure::new(("output-group", row.0 as u64), !is_folded)
|
||||
.toggle_state(is_folded)
|
||||
.on_click(move |_event, window, cx| fold(!is_folded, window, cx))
|
||||
.into_any_element()
|
||||
},
|
||||
move |_id, _range, _window, _cx| gpui::Empty.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn evaluate(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let expression = self.query_bar.update(cx, |editor, cx| {
|
||||
let expression = editor.text(cx);
|
||||
|
||||
editor.clear(window, cx);
|
||||
|
||||
expression
|
||||
});
|
||||
|
||||
self.session.update(cx, |state, cx| {
|
||||
state.evaluate(
|
||||
expression,
|
||||
Some(dap::EvaluateArgumentsContext::Variables),
|
||||
Some(self.stack_frame_list.read(cx).current_stack_frame_id()),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_console(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.console.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_features: settings.buffer_font.features.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.console,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.console.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: TextSize::Editor.rems(cx).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
&self.query_bar,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Console {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("DebugConsole")
|
||||
.on_action(cx.listener(Self::evaluate))
|
||||
.size_full()
|
||||
.child(self.render_console(cx))
|
||||
.when(self.is_local(cx), |this| {
|
||||
this.child(self.render_query_bar(cx))
|
||||
.pt(DynamicSpacing::Base04.rems(cx))
|
||||
})
|
||||
.border_2()
|
||||
}
|
||||
}
|
||||
|
||||
struct ConsoleQueryBarCompletionProvider(WeakEntity<Console>);
|
||||
|
||||
impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
_trigger: editor::CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<gpui::Result<Vec<project::Completion>>> {
|
||||
let Some(console) = self.0.upgrade() else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let support_completions = console
|
||||
.read(cx)
|
||||
.session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_completions_request
|
||||
.unwrap_or_default();
|
||||
|
||||
if support_completions {
|
||||
self.client_completions(&console, buffer, buffer_position, cx)
|
||||
} else {
|
||||
self.variable_list_completions(&console, buffer, buffer_position, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_completions(
|
||||
&self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_completion_indices: Vec<usize>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<gpui::Result<bool>> {
|
||||
Task::ready(Ok(false))
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_completion_index: usize,
|
||||
_push_to_history: bool,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<gpui::Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
_buffer: &Entity<Buffer>,
|
||||
_position: language::Anchor,
|
||||
_text: &str,
|
||||
_trigger_in_words: bool,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl ConsoleQueryBarCompletionProvider {
|
||||
fn variable_list_completions(
|
||||
&self,
|
||||
console: &Entity<Console>,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<gpui::Result<Vec<project::Completion>>> {
|
||||
let (variables, string_matches) = console.update(cx, |console, cx| {
|
||||
let mut variables = HashMap::new();
|
||||
let mut string_matches = Vec::new();
|
||||
|
||||
for variable in console.variable_list.update(cx, |variable_list, cx| {
|
||||
variable_list.completion_variables(cx)
|
||||
}) {
|
||||
if let Some(evaluate_name) = &variable.evaluate_name {
|
||||
variables.insert(evaluate_name.clone(), variable.value.clone());
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: evaluate_name.clone(),
|
||||
char_bag: evaluate_name.chars().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
variables.insert(variable.name.clone(), variable.value.clone());
|
||||
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: variable.name.clone(),
|
||||
char_bag: variable.name.chars().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
(variables, string_matches)
|
||||
});
|
||||
|
||||
let query = buffer.read(cx).text();
|
||||
|
||||
cx.spawn(|_, cx| async move {
|
||||
let matches = fuzzy::match_strings(
|
||||
&string_matches,
|
||||
&query,
|
||||
true,
|
||||
10,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(matches
|
||||
.iter()
|
||||
.filter_map(|string_match| {
|
||||
let variable_value = variables.get(&string_match.string)?;
|
||||
|
||||
Some(project::Completion {
|
||||
old_range: buffer_position..buffer_position,
|
||||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..string_match.string.len(),
|
||||
text: format!("{} {}", string_match.string.clone(), variable_value),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn client_completions(
|
||||
&self,
|
||||
console: &Entity<Console>,
|
||||
buffer: &Entity<Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<gpui::Result<Vec<project::Completion>>> {
|
||||
let completion_task = console.update(cx, |console, cx| {
|
||||
console.session.update(cx, |state, cx| {
|
||||
let frame_id = Some(console.stack_frame_list.read(cx).current_stack_frame_id());
|
||||
|
||||
state.completions(
|
||||
CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
Ok(completion_task
|
||||
.await?
|
||||
.iter()
|
||||
.map(|completion| project::Completion {
|
||||
old_range: buffer_position..buffer_position, // TODO(debugger): change this
|
||||
new_text: completion.text.clone().unwrap_or(completion.label.clone()),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..completion.label.len(),
|
||||
text: completion.label.clone(),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
server_id: LanguageServerId(usize::MAX),
|
||||
documentation: None,
|
||||
lsp_completion: Default::default(),
|
||||
confirm: None,
|
||||
resolved: true,
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
}
|
||||
95
crates/debugger_ui/src/session/running/loaded_source_list.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription};
|
||||
use project::debugger::session::Session;
|
||||
use ui::prelude::*;
|
||||
use util::maybe;
|
||||
|
||||
pub struct LoadedSourceList {
|
||||
list: ListState,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
}
|
||||
|
||||
impl LoadedSourceList {
|
||||
pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_entity
|
||||
.upgrade()
|
||||
.map(|loaded_sources| {
|
||||
loaded_sources.update(cx, |this, cx| this.render_entry(ix, cx))
|
||||
})
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
|
||||
let _subscription = cx.observe(&session, |loaded_source_list, state, cx| {
|
||||
let len = state.update(cx, |state, cx| state.loaded_sources(cx).len());
|
||||
|
||||
loaded_source_list.list.reset(len);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Self {
|
||||
list,
|
||||
session,
|
||||
focus_handle,
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(source) = maybe!({
|
||||
self.session
|
||||
.update(cx, |state, cx| state.loaded_sources(cx).get(ix).cloned())
|
||||
}) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.w_full()
|
||||
.group("")
|
||||
.p_1()
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.text_ui_sm(cx)
|
||||
.when_some(source.name.clone(), |this, name| this.child(name)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(source.path.clone(), |this, path| this.child(path)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for LoadedSourceList {
|
||||
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for LoadedSourceList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.session.update(cx, |state, cx| {
|
||||
state.loaded_sources(cx);
|
||||
});
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
}
|
||||
}
|
||||
100
crates/debugger_ui/src/session/running/module_list.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use gpui::{list, AnyElement, Empty, Entity, FocusHandle, Focusable, ListState, Subscription};
|
||||
use project::debugger::session::Session;
|
||||
use ui::prelude::*;
|
||||
|
||||
pub struct ModuleList {
|
||||
list: ListState,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
}
|
||||
|
||||
impl ModuleList {
|
||||
pub fn new(session: Entity<Session>, cx: &mut Context<Self>) -> Self {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_entity
|
||||
.upgrade()
|
||||
.map(|module_list| module_list.update(cx, |this, cx| this.render_entry(ix, cx)))
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
|
||||
let _subscription = cx.observe(&session, |module_list, state, cx| {
|
||||
let modules_len = state.update(cx, |state, cx| state.modules(cx).len());
|
||||
|
||||
module_list.list.reset(modules_len);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Self {
|
||||
list,
|
||||
session,
|
||||
focus_handle,
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(module) = maybe!({
|
||||
self.session
|
||||
.update(cx, |state, cx| state.modules(cx).get(ix).cloned())
|
||||
}) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
.w_full()
|
||||
.group("")
|
||||
.p_1()
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover))
|
||||
.child(h_flex().gap_0p5().text_ui_sm(cx).child(module.name.clone()))
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(module.path.clone(), |this, path| this.child(path)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ModuleList {
|
||||
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ModuleList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.session.update(cx, |state, cx| {
|
||||
state.modules(cx);
|
||||
});
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use dap::Module;
|
||||
use util::maybe;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl ModuleList {
|
||||
pub fn modules(&self, cx: &mut Context<Self>) -> Vec<Module> {
|
||||
self.session.update(cx, |session, cx| {
|
||||
session.modules(cx).iter().cloned().collect()
|
||||
})
|
||||
}
|
||||
}
|
||||
483
crates/debugger_ui/src/session/running/stack_frame_list.rs
Normal file
@@ -0,0 +1,483 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use dap::StackFrameId;
|
||||
use gpui::{
|
||||
list, AnyElement, Entity, EventEmitter, FocusHandle, Focusable, ListState, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
|
||||
use language::Point;
|
||||
use project::debugger::session::{Session, StackFrame, ThreadId};
|
||||
use project::{ProjectItem, ProjectPath};
|
||||
use ui::{prelude::*, Tooltip};
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StackFrameListEvent {
|
||||
SelectedStackFrameChanged(StackFrameId),
|
||||
}
|
||||
|
||||
pub struct StackFrameList {
|
||||
list: ListState,
|
||||
thread_id: Option<ThreadId>,
|
||||
focus_handle: FocusHandle,
|
||||
_subscription: Subscription,
|
||||
session: Entity<Session>,
|
||||
entries: Vec<StackFrameEntry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
current_stack_frame_id: StackFrameId,
|
||||
_fetch_stack_frames_task: Option<Task<Result<()>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum StackFrameEntry {
|
||||
Normal(dap::StackFrame),
|
||||
Collapsed(Vec<dap::StackFrame>),
|
||||
}
|
||||
|
||||
impl StackFrameList {
|
||||
pub fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
session: Entity<Session>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let weak_entity = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_entity
|
||||
.upgrade()
|
||||
.map(|stack_frame_list| {
|
||||
stack_frame_list.update(cx, |this, cx| this.render_entry(ix, cx))
|
||||
})
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
|
||||
let _subscription = cx.observe(&session, |stack_frame_list, _, cx| {
|
||||
stack_frame_list.build_entries(cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
list,
|
||||
session,
|
||||
workspace,
|
||||
focus_handle,
|
||||
_subscription,
|
||||
thread_id: None,
|
||||
entries: Default::default(),
|
||||
_fetch_stack_frames_task: None,
|
||||
current_stack_frame_id: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_thread_id(&mut self, thread_id: Option<ThreadId>, cx: &mut Context<Self>) {
|
||||
self.thread_id = thread_id;
|
||||
self.build_entries(cx);
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn entries(&self) -> &Vec<StackFrameEntry> {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
pub fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
|
||||
self.thread_id
|
||||
.map(|thread_id| {
|
||||
self.session
|
||||
.update(cx, |this, cx| this.stack_frames(thread_id, cx))
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
|
||||
self.stack_frames(cx)
|
||||
.into_iter()
|
||||
.map(|stack_frame| stack_frame.dap.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn _get_main_stack_frame_id(&self, cx: &mut Context<Self>) -> u64 {
|
||||
self.stack_frames(cx)
|
||||
.first()
|
||||
.map(|stack_frame| stack_frame.dap.id)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub fn current_stack_frame_id(&self) -> u64 {
|
||||
self.current_stack_frame_id
|
||||
}
|
||||
|
||||
pub fn _current_thread_id(&self) -> Option<ThreadId> {
|
||||
self.thread_id
|
||||
}
|
||||
|
||||
fn build_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let mut entries = Vec::new();
|
||||
let mut collapsed_entries = Vec::new();
|
||||
|
||||
for stack_frame in &self.stack_frames(cx) {
|
||||
match stack_frame.dap.presentation_hint {
|
||||
Some(dap::StackFramePresentationHint::Deemphasize) => {
|
||||
collapsed_entries.push(stack_frame.dap.clone());
|
||||
}
|
||||
_ => {
|
||||
let collapsed_entries = std::mem::take(&mut collapsed_entries);
|
||||
if !collapsed_entries.is_empty() {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
|
||||
}
|
||||
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let collapsed_entries = std::mem::take(&mut collapsed_entries);
|
||||
if !collapsed_entries.is_empty() {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
|
||||
}
|
||||
|
||||
std::mem::swap(&mut self.entries, &mut entries);
|
||||
self.list.reset(self.entries.len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
// fn fetch_stack_frames(
|
||||
// &mut self,
|
||||
// go_to_stack_frame: bool,
|
||||
// window: &Window,
|
||||
// cx: &mut Context<Self>,
|
||||
// ) {
|
||||
// // If this is a remote debug session we never need to fetch stack frames ourselves
|
||||
// // because the host will fetch and send us stack frames whenever there's a stop event
|
||||
// if self.dap_store.read(cx).as_remote().is_some() {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let task = self.dap_store.update(cx, |store, cx| {
|
||||
// store.stack_frames(&self.client_id, self.thread_id, cx)
|
||||
// });
|
||||
|
||||
// self.fetch_stack_frames_task = Some(cx.spawn_in(window, |this, mut cx| async move {
|
||||
// let mut stack_frames = task.await?;
|
||||
|
||||
// let task = this.update_in(&mut cx, |this, window, cx| {
|
||||
// std::mem::swap(&mut this.stack_frames, &mut stack_frames);
|
||||
|
||||
// this.build_entries();
|
||||
|
||||
// cx.emit(StackFrameListEvent::StackFramesUpdated);
|
||||
|
||||
// let stack_frame = this
|
||||
// .stack_frames
|
||||
// .first()
|
||||
// .cloned()
|
||||
// .ok_or_else(|| anyhow!("No stack frame found to select"))?;
|
||||
|
||||
// anyhow::Ok(this.select_stack_frame(&stack_frame, go_to_stack_frame, window, cx))
|
||||
// })?;
|
||||
|
||||
// task?.await?;
|
||||
|
||||
// this.update(&mut cx, |this, _| {
|
||||
// this.fetch_stack_frames_task.take();
|
||||
// })
|
||||
// }));
|
||||
// }
|
||||
|
||||
pub fn select_stack_frame(
|
||||
&mut self,
|
||||
stack_frame: &dap::StackFrame,
|
||||
go_to_stack_frame: bool,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.current_stack_frame_id = stack_frame.id;
|
||||
|
||||
cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
|
||||
stack_frame.id,
|
||||
));
|
||||
cx.notify();
|
||||
|
||||
if !go_to_stack_frame {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
|
||||
let row = (stack_frame.line.saturating_sub(1)) as u32;
|
||||
|
||||
let Some(abs_path) = self.abs_path_from_stack_frame(&stack_frame, cx) else {
|
||||
return Task::ready(Err(anyhow!("Project path not found")));
|
||||
};
|
||||
|
||||
cx.spawn_in(window, {
|
||||
// let client_id = self.client_id;
|
||||
move |this, mut cx| async move {
|
||||
let buffer = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |this, cx| this.open_local_buffer(abs_path.clone(), cx))
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
let position = buffer.update(&mut cx, |this, _| {
|
||||
this.snapshot().anchor_before(Point::new(row, 0))
|
||||
})?;
|
||||
this.update_in(&mut cx, |this, window, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
let project_path = buffer.read(cx).project_path(cx).ok_or_else(|| {
|
||||
anyhow!("Could not select a stack frame for unnamed buffer")
|
||||
})?;
|
||||
Result::<_, anyhow::Error>::Ok(workspace.open_path_preview(
|
||||
project_path,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
})???
|
||||
.await?;
|
||||
|
||||
// TODO(debugger): make this work again
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
let breakpoint_store = workspace.project().read(cx).breakpoint_store();
|
||||
|
||||
breakpoint_store.update(cx, |store, cx| {
|
||||
let _ = store.set_active_position(Some((abs_path, position)));
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
})?
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn abs_path_from_stack_frame(
|
||||
&self,
|
||||
stack_frame: &dap::StackFrame,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Arc<Path>> {
|
||||
stack_frame.source.as_ref().and_then(|s| {
|
||||
s.path
|
||||
.as_deref()
|
||||
.map(|path| Arc::<Path>::from(Path::new(path)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
|
||||
self.session.update(cx, |state, cx| {
|
||||
state.restart_stack_frame(stack_frame_id, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn render_normal_entry(
|
||||
&self,
|
||||
stack_frame: &dap::StackFrame,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let source = stack_frame.source.clone();
|
||||
let is_selected_frame = stack_frame.id == self.current_stack_frame_id;
|
||||
|
||||
let formatted_path = format!(
|
||||
"{}:{}",
|
||||
source.clone().and_then(|s| s.name).unwrap_or_default(),
|
||||
stack_frame.line,
|
||||
);
|
||||
|
||||
let supports_frame_restart = self
|
||||
.session
|
||||
.read(cx)
|
||||
.capabilities()
|
||||
.supports_restart_frame
|
||||
.unwrap_or_default();
|
||||
|
||||
let origin = stack_frame
|
||||
.source
|
||||
.to_owned()
|
||||
.and_then(|source| source.origin);
|
||||
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("stack-frame", stack_frame.id))
|
||||
.tooltip({
|
||||
let formatted_path = formatted_path.clone();
|
||||
move |_window, app| {
|
||||
app.new(|_| {
|
||||
let mut tooltip = Tooltip::new(formatted_path.clone());
|
||||
|
||||
if let Some(origin) = &origin {
|
||||
tooltip = tooltip.meta(origin);
|
||||
}
|
||||
|
||||
tooltip
|
||||
})
|
||||
.into()
|
||||
}
|
||||
})
|
||||
.p_1()
|
||||
.when(is_selected_frame, |this| {
|
||||
this.bg(cx.theme().colors().element_hover)
|
||||
})
|
||||
.on_click(cx.listener({
|
||||
let stack_frame = stack_frame.clone();
|
||||
move |this, _, window, cx| {
|
||||
this.select_stack_frame(&stack_frame, true, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.text_ui_sm(cx)
|
||||
.truncate()
|
||||
.child(stack_frame.name.clone())
|
||||
.child(formatted_path),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_xs(cx)
|
||||
.truncate()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
|
||||
),
|
||||
)
|
||||
.when(
|
||||
supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
|
||||
|this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.id(("restart-stack-frame", stack_frame.id))
|
||||
.visible_on_hover("")
|
||||
.absolute()
|
||||
.right_2()
|
||||
.overflow_hidden()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().element_selected)
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(cx.theme().colors().ghost_element_hover)
|
||||
.cursor_pointer()
|
||||
})
|
||||
.child(
|
||||
IconButton::new(
|
||||
("restart-stack-frame", stack_frame.id),
|
||||
IconName::DebugRestart,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let stack_frame_id = stack_frame.id;
|
||||
move |this, _, _window, cx| {
|
||||
this.restart_stack_frame(stack_frame_id, cx);
|
||||
}
|
||||
}))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::text("Restart Stack Frame")(window, cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub fn expand_collapsed_entry(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
stack_frames: &Vec<dap::StackFrame>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.entries.splice(
|
||||
ix..ix + 1,
|
||||
stack_frames
|
||||
.iter()
|
||||
.map(|frame| StackFrameEntry::Normal(frame.clone())),
|
||||
);
|
||||
self.list.reset(self.entries.len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_collapsed_entry(
|
||||
&self,
|
||||
ix: usize,
|
||||
stack_frames: &Vec<dap::StackFrame>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let first_stack_frame = &stack_frames[0];
|
||||
|
||||
h_flex()
|
||||
.rounded_md()
|
||||
.justify_between()
|
||||
.w_full()
|
||||
.group("")
|
||||
.id(("stack-frame", first_stack_frame.id))
|
||||
.p_1()
|
||||
.on_click(cx.listener({
|
||||
let stack_frames = stack_frames.clone();
|
||||
move |this, _, _window, cx| {
|
||||
this.expand_collapsed_entry(ix, &stack_frames, cx);
|
||||
}
|
||||
}))
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
|
||||
.child(
|
||||
v_flex()
|
||||
.text_ui_sm(cx)
|
||||
.truncate()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(format!(
|
||||
"Show {} more{}",
|
||||
stack_frames.len(),
|
||||
first_stack_frame
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|source| source.origin.as_ref())
|
||||
.map_or(String::new(), |origin| format!(": {}", origin))
|
||||
)),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
match &self.entries[ix] {
|
||||
StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
|
||||
StackFrameEntry::Collapsed(stack_frames) => {
|
||||
self.render_collapsed_entry(ix, stack_frames, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StackFrameList {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.p_1()
|
||||
.child(list(self.list.clone()).size_full())
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for StackFrameList {
|
||||
fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<StackFrameListEvent> for StackFrameList {}
|
||||
839
crates/debugger_ui/src/session/running/variable_list.rs
Normal file
@@ -0,0 +1,839 @@
|
||||
use super::stack_frame_list::{StackFrameList, StackFrameListEvent};
|
||||
use dap::{StackFrameId, VariableReference};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use gpui::{
|
||||
actions, anchored, deferred, list, AnyElement, Context, Entity, FocusHandle, Focusable, Hsla,
|
||||
ListState, MouseDownEvent, Point, Subscription,
|
||||
};
|
||||
use project::debugger::session::Session;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use ui::{prelude::*, ContextMenu, ListItem};
|
||||
use util::debug_panic;
|
||||
|
||||
actions!(variable_list, [ExpandSelectedEntry, CollapseSelectedEntry]);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
struct VariableState {
|
||||
depth: usize,
|
||||
is_expanded: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ScopeState {
|
||||
is_expanded: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
struct VariablePath {
|
||||
base: VariableReference,
|
||||
indices: Arc<[VariableReference]>,
|
||||
}
|
||||
|
||||
enum VariableListEntry {
|
||||
Scope((dap::Scope, ScopeState)),
|
||||
Variable((dap::Variable, VariablePath, VariableState)),
|
||||
}
|
||||
|
||||
pub struct VariableList {
|
||||
entries: Vec<VariableListEntry>,
|
||||
scope_states: HashMap<StackFrameId, ScopeState>,
|
||||
variable_states: HashMap<VariablePath, VariableState>,
|
||||
selected_stack_frame_id: Option<StackFrameId>,
|
||||
list: ListState,
|
||||
session: Entity<Session>,
|
||||
_selection: Option<VariableListEntry>,
|
||||
open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
focus_handle: FocusHandle,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl VariableList {
|
||||
pub fn new(
|
||||
session: Entity<Session>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let weak_variable_list = cx.weak_entity();
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let list = ListState::new(
|
||||
0,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1000.),
|
||||
move |ix, _window, cx| {
|
||||
weak_variable_list
|
||||
.upgrade()
|
||||
.map(|var_list| var_list.update(cx, |this, cx| this.render_entry(ix, cx)))
|
||||
.unwrap_or(div().into_any())
|
||||
},
|
||||
);
|
||||
list.set_scroll_handler(|_, _, _| {
|
||||
dbg!("Scrolling");
|
||||
});
|
||||
|
||||
let set_variable_editor = cx.new(|cx| Editor::single_line(window, cx));
|
||||
|
||||
cx.subscribe(
|
||||
&set_variable_editor,
|
||||
|_this: &mut Self, _, event: &EditorEvent, _cx| {
|
||||
if *event == EditorEvent::Blurred {
|
||||
// this.cancel_set_variable_value(cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
|
||||
cx.subscribe(&session, |this, _, _, cx| {
|
||||
this.build_entries(cx);
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
list,
|
||||
session,
|
||||
focus_handle,
|
||||
_subscriptions,
|
||||
selected_stack_frame_id: None,
|
||||
_selection: None,
|
||||
open_context_menu: None,
|
||||
entries: Default::default(),
|
||||
scope_states: Default::default(),
|
||||
variable_states: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let Some(stack_frame_id) = self.selected_stack_frame_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut entries = vec![];
|
||||
let scopes: Vec<_> = self.session.update(cx, |session, cx| {
|
||||
session.scopes(stack_frame_id, cx).iter().cloned().collect()
|
||||
});
|
||||
|
||||
fn inner(
|
||||
this: &mut VariableList,
|
||||
variable_reference: VariableReference,
|
||||
indices: &mut Vec<u64>,
|
||||
entries: &mut Vec<VariableListEntry>,
|
||||
cx: &mut Context<VariableList>,
|
||||
) {
|
||||
for variable in this
|
||||
.session
|
||||
.update(cx, |session, cx| session.variables(variable_reference, cx))
|
||||
{
|
||||
let child_ref = variable.variables_reference;
|
||||
let depth = indices.len() + 1;
|
||||
|
||||
let var_path = VariablePath {
|
||||
base: child_ref,
|
||||
indices: Arc::from(indices.clone()),
|
||||
};
|
||||
|
||||
let var_state =
|
||||
*this
|
||||
.variable_states
|
||||
.entry(var_path.clone())
|
||||
.or_insert(VariableState {
|
||||
depth,
|
||||
is_expanded: false,
|
||||
});
|
||||
|
||||
entries.push(VariableListEntry::Variable((variable, var_path, var_state)));
|
||||
|
||||
indices.push(child_ref);
|
||||
if var_state.is_expanded {
|
||||
inner(this, child_ref, indices, entries, cx);
|
||||
}
|
||||
indices.pop();
|
||||
}
|
||||
}
|
||||
let mut indices = Vec::new();
|
||||
|
||||
for scope in scopes.iter().cloned() {
|
||||
let state = *self
|
||||
.scope_states
|
||||
.entry(scope.variables_reference)
|
||||
.or_insert(ScopeState { is_expanded: true });
|
||||
|
||||
let scope_ref = scope.variables_reference;
|
||||
|
||||
entries.push(VariableListEntry::Scope((scope, state)));
|
||||
|
||||
indices.push(scope_ref);
|
||||
|
||||
if state.is_expanded {
|
||||
inner(self, scope_ref, &mut indices, &mut entries, cx);
|
||||
}
|
||||
indices.pop();
|
||||
}
|
||||
|
||||
self.entries = entries;
|
||||
self.list.reset(self.entries.len());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_stack_frame_list_events(
|
||||
&mut self,
|
||||
_: Entity<StackFrameList>,
|
||||
event: &StackFrameListEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
|
||||
self.selected_stack_frame_id = Some(*stack_frame_id);
|
||||
self.build_entries(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// debugger(todo): This only returns visible variables will need to change it to show all variables
|
||||
// within a stack frame scope
|
||||
pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> {
|
||||
self.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
VariableListEntry::Variable((variable, ..)) => Some(variable.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_entry(&mut self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(entry) = self.entries.get(ix) else {
|
||||
debug_panic!("Trying to render entry in variable list that has an out of bounds index");
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
// todo(debugger) pass a valid value for is selected
|
||||
let is_selected = false;
|
||||
|
||||
match entry {
|
||||
VariableListEntry::Scope((scope, state)) => {
|
||||
self.render_scope(scope.clone(), *state, is_selected, cx)
|
||||
}
|
||||
VariableListEntry::Variable((variable, var_path, state)) => self.render_variable(
|
||||
variable.clone(),
|
||||
var_path.to_owned(),
|
||||
*state,
|
||||
is_selected,
|
||||
cx,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_scope(&mut self, scope_ref: VariableReference, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.scope_states.get_mut(&scope_ref) else {
|
||||
debug_panic!("Trying to toggle scope in variable list that has an no state");
|
||||
return;
|
||||
};
|
||||
|
||||
entry.is_expanded = !entry.is_expanded;
|
||||
self.build_entries(cx);
|
||||
}
|
||||
|
||||
fn toggle_variable(&mut self, var_path: &VariablePath, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.variable_states.get_mut(var_path) else {
|
||||
debug_panic!("Trying to toggle variable in variable list that has an no state");
|
||||
return;
|
||||
};
|
||||
|
||||
entry.is_expanded = !entry.is_expanded;
|
||||
self.build_entries(cx);
|
||||
}
|
||||
|
||||
// fn select_first(&mut self, _: &SelectFirst, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
// let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id();
|
||||
// if let Some(entries) = self.entries.get(&stack_frame_id) {
|
||||
// self.selection = entries.first().cloned();
|
||||
// cx.notify();
|
||||
// };
|
||||
// }
|
||||
|
||||
// fn select_last(&mut self, _: &SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
// let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id();
|
||||
// if let Some(entries) = self.entries.get(&stack_frame_id) {
|
||||
// self.selection = entries.last().cloned();
|
||||
// cx.notify();
|
||||
// };
|
||||
// }
|
||||
|
||||
// // fn select_prev(&mut self, _: &SelectPrev, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// // if let Some(selection) = &self.selection {
|
||||
// // let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id();
|
||||
// // if let Some(entries) = self.entries.get(&stack_frame_id) {
|
||||
// // if let Some(ix) = entries.iter().position(|entry| entry == selection) {
|
||||
// // self.selection = entries.get(ix.saturating_sub(1)).cloned();
|
||||
// // cx.notify();
|
||||
// // }
|
||||
// // }
|
||||
// // } else {
|
||||
// // self.select_first(&SelectFirst, window, cx);
|
||||
// // }
|
||||
// // }
|
||||
|
||||
// fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// if let Some(selection) = &self.selection {
|
||||
// let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id();
|
||||
// if let Some(entries) = self.entries.get(&stack_frame_id) {
|
||||
// if let Some(ix) = entries.iter().position(|entry| entry == selection) {
|
||||
// self.selection = entries.get(ix + 1).cloned();
|
||||
// cx.notify();
|
||||
// }
|
||||
// }
|
||||
// } else {
|
||||
// self.select_first(&SelectFirst, window, cx);
|
||||
// }
|
||||
// }
|
||||
|
||||
fn _collapse_selected_entry(
|
||||
&mut self,
|
||||
_: &CollapseSelectedEntry,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
// if let Some(selection) = &self.selection {
|
||||
// match selection {
|
||||
// VariableListEntry::Scope(scope) => {
|
||||
// let entry_id = &OpenEntry::Scope {
|
||||
// name: scope.name.clone(),
|
||||
// };
|
||||
|
||||
// if self.open_entries.binary_search(entry_id).is_err() {
|
||||
// self.select_prev(&SelectPrev, window, cx);
|
||||
// } else {
|
||||
// self.toggle_entry(entry_id, cx);
|
||||
// }
|
||||
// }
|
||||
// VariableListEntry::Variable {
|
||||
// depth,
|
||||
// variable,
|
||||
// scope,
|
||||
// ..
|
||||
// } => {
|
||||
// let entry_id = &OpenEntry::Variable {
|
||||
// depth: *depth,
|
||||
// name: variable.name.clone(),
|
||||
// scope_name: scope.name.clone(),
|
||||
// };
|
||||
|
||||
// if self.open_entries.binary_search(entry_id).is_err() {
|
||||
// self.select_prev(&SelectPrev, window, cx);
|
||||
// } else {
|
||||
// // todo
|
||||
// }
|
||||
// }
|
||||
// VariableListEntry::SetVariableEditor { .. } => {}
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
fn _expand_selected_entry(
|
||||
&mut self,
|
||||
_: &ExpandSelectedEntry,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
|
||||
// todo(debugger) Implement expand_selected_entry
|
||||
// if let Some(selection) = &self.selection {
|
||||
// match selection {
|
||||
// VariableListEntry::Scope(scope) => {
|
||||
// let entry_id = &OpenEntry::Scope {
|
||||
// name: scope.name.clone(),
|
||||
// };
|
||||
|
||||
// if self.open_entries.binary_search(entry_id).is_ok() {
|
||||
// self.select_next(&SelectNext, window, cx);
|
||||
// } else {
|
||||
// self.toggle_entry(entry_id, cx);
|
||||
// }
|
||||
// }
|
||||
// VariableListEntry::Variable {
|
||||
// depth,
|
||||
// variable,
|
||||
// scope,
|
||||
// ..
|
||||
// } => {
|
||||
// let entry_id = &OpenEntry::Variable {
|
||||
// depth: *depth,
|
||||
// name: variable.dap.name.clone(),
|
||||
// scope_name: scope.name.clone(),
|
||||
// };
|
||||
|
||||
// if self.open_entries.binary_search(entry_id).is_ok() {
|
||||
// self.select_next(&SelectNext, window, cx);
|
||||
// } else {
|
||||
// // self.toggle_variable(&scope.clone(), &variable.clone(), *depth, cx);
|
||||
// }
|
||||
// }
|
||||
// VariableListEntry::SetVariableEditor { .. } => {}
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn assert_visual_entries(&self, expected: Vec<&str>, cx: &Context<Self>) {
|
||||
unimplemented!("Will finish after refactor is done");
|
||||
// const INDENT: &'static str = " ";
|
||||
|
||||
// let stack_frame_id = self.stack_frame_list.read(cx).current_stack_frame_id();
|
||||
// let entries = self.entries.get(&stack_frame_id).unwrap();
|
||||
|
||||
// let mut visual_entries = Vec::with_capacity(entries.len());
|
||||
// for entry in entries {
|
||||
// let is_selected = Some(entry) == self.selection.as_ref();
|
||||
|
||||
// match entry {
|
||||
// VariableListEntry::Scope(scope) => {
|
||||
// let is_expanded = self
|
||||
// .open_entries
|
||||
// .binary_search(&OpenEntry::Scope {
|
||||
// name: scope.name.clone(),
|
||||
// })
|
||||
// .is_ok();
|
||||
|
||||
// visual_entries.push(format!(
|
||||
// "{} {}{}",
|
||||
// if is_expanded { "v" } else { ">" },
|
||||
// scope.name,
|
||||
// if is_selected { " <=== selected" } else { "" }
|
||||
// ));
|
||||
// }
|
||||
// VariableListEntry::SetVariableEditor { depth, state } => {
|
||||
// visual_entries.push(format!(
|
||||
// "{} [EDITOR: {}]{}",
|
||||
// INDENT.repeat(*depth),
|
||||
// state.name,
|
||||
// if is_selected { " <=== selected" } else { "" }
|
||||
// ));
|
||||
// }
|
||||
// VariableListEntry::Variable {
|
||||
// depth,
|
||||
// variable,
|
||||
// scope,
|
||||
// ..
|
||||
// } => {
|
||||
// let is_expanded = self
|
||||
// .open_entries
|
||||
// .binary_search(&OpenEntry::Variable {
|
||||
// depth: *depth,
|
||||
// name: variable.name.clone(),
|
||||
// scope_name: scope.name.clone(),
|
||||
// })
|
||||
// .is_ok();
|
||||
|
||||
// visual_entries.push(format!(
|
||||
// "{}{} {}{}",
|
||||
// INDENT.repeat(*depth),
|
||||
// if is_expanded { "v" } else { ">" },
|
||||
// variable.name,
|
||||
// if is_selected { " <=== selected" } else { "" }
|
||||
// ));
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
|
||||
// pretty_assertions::assert_eq!(expected, visual_entries);
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_variable(
|
||||
&self,
|
||||
variable: dap::Variable,
|
||||
var_path: VariablePath,
|
||||
state: VariableState,
|
||||
is_selected: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let var_ref = variable.variables_reference;
|
||||
let colors = _get_entry_color(cx);
|
||||
let bg_hover_color = if !is_selected {
|
||||
colors.hover
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
let border_color = if is_selected {
|
||||
colors.marked_active
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
|
||||
div()
|
||||
.id(SharedString::from(format!(
|
||||
"variable-{}-{}",
|
||||
variable.name, state.depth
|
||||
)))
|
||||
.group("variable_list_entry")
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.border_color(border_color)
|
||||
.h_4()
|
||||
.size_full()
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
.on_click(cx.listener({
|
||||
// let scope = scope.clone();
|
||||
// let variable = variable.clone();
|
||||
move |_this, _, _window, _cx| {
|
||||
// this.selection = Some(VariableListEntry::Variable {
|
||||
// depth,
|
||||
// has_children,
|
||||
// container_reference,
|
||||
// scope: scope.clone(),
|
||||
// variable: variable.clone(),
|
||||
// });
|
||||
// cx.notify();
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!(
|
||||
"variable-item-{}-{}",
|
||||
variable.name, state.depth
|
||||
)))
|
||||
.selectable(false)
|
||||
.indent_level(state.depth as usize)
|
||||
.indent_step_size(px(20.))
|
||||
.always_show_disclosure_icon(true)
|
||||
.when(var_ref > 0, |list_item| {
|
||||
list_item.toggle(state.is_expanded).on_toggle(cx.listener({
|
||||
let var_path = var_path.clone();
|
||||
move |this, _, _window, cx| {
|
||||
this.session.update(cx, |session, cx| {
|
||||
session.variables(var_ref, cx);
|
||||
});
|
||||
|
||||
this.toggle_variable(&var_path, cx);
|
||||
}
|
||||
}))
|
||||
})
|
||||
.on_secondary_mouse_down(cx.listener({
|
||||
// let scope = scope.clone();
|
||||
// let variable = variable.clone();
|
||||
move |_this, _event: &MouseDownEvent, _window, _cx| {
|
||||
|
||||
// todo(debugger): Get this working
|
||||
// this.deploy_variable_context_menu(
|
||||
// container_reference,
|
||||
// &scope,
|
||||
// &variable,
|
||||
// event.position,
|
||||
// window,
|
||||
// cx,
|
||||
// )
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_ui_sm(cx)
|
||||
.child(variable.name.clone())
|
||||
.child(
|
||||
div()
|
||||
.text_ui_xs(cx)
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(variable.value.replace("\n", " ").clone()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_scope(
|
||||
&self,
|
||||
scope: dap::Scope,
|
||||
state: ScopeState,
|
||||
is_selected: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let var_ref = scope.variables_reference;
|
||||
|
||||
let colors = _get_entry_color(cx);
|
||||
let bg_hover_color = if !is_selected {
|
||||
colors.hover
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
let border_color = if is_selected {
|
||||
colors.marked_active
|
||||
} else {
|
||||
colors.default
|
||||
};
|
||||
|
||||
div()
|
||||
.id(var_ref as usize)
|
||||
.group("variable_list_entry")
|
||||
.border_1()
|
||||
.border_r_2()
|
||||
.border_color(border_color)
|
||||
.flex()
|
||||
.w_full()
|
||||
.h_full()
|
||||
.hover(|style| style.bg(bg_hover_color))
|
||||
.on_click(cx.listener({
|
||||
move |_this, _, _window, cx| {
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
ListItem::new(SharedString::from(format!("scope-{}", var_ref)))
|
||||
.selectable(false)
|
||||
.indent_level(1)
|
||||
.indent_step_size(px(20.))
|
||||
.always_show_disclosure_icon(true)
|
||||
.toggle(state.is_expanded)
|
||||
.on_toggle(cx.listener(move |this, _, _, cx| this.toggle_scope(var_ref, cx)))
|
||||
.child(div().text_ui(cx).w_full().child(scope.name.clone())),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for VariableList {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for VariableList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("VariableList")
|
||||
.id("variable-list")
|
||||
.group("variable-list")
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
// .on_action(cx.listener(Self::select_first))
|
||||
// .on_action(cx.listener(Self::select_last))
|
||||
// .on_action(cx.listener(Self::select_prev))
|
||||
// .on_action(cx.listener(Self::select_next))
|
||||
// .on_action(cx.listener(Self::expand_selected_entry))
|
||||
// .on_action(cx.listener(Self::collapse_selected_entry))
|
||||
//
|
||||
.on_action(
|
||||
cx.listener(|_this, _: &editor::actions::Cancel, _window, _cx| {
|
||||
|
||||
// this.cancel_set_variable_value(cx)
|
||||
}),
|
||||
)
|
||||
.child(list(self.list.clone()).gap_1_5().size_full().flex_grow())
|
||||
.children(self.open_context_menu.as_ref().map(|(menu, position, _)| {
|
||||
deferred(
|
||||
anchored()
|
||||
.position(*position)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.child(menu.clone()),
|
||||
)
|
||||
.with_priority(1)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
struct _EntryColors {
|
||||
default: Hsla,
|
||||
hover: Hsla,
|
||||
marked_active: Hsla,
|
||||
}
|
||||
|
||||
fn _get_entry_color(cx: &Context<VariableList>) -> _EntryColors {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
_EntryColors {
|
||||
default: colors.panel_background,
|
||||
hover: colors.ghost_element_hover,
|
||||
marked_active: colors.ghost_element_selected,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add_initial_variables_to_index() {
|
||||
unimplemented!("This test is commented out")
|
||||
// let mut index = ScopeVariableIndex::new();
|
||||
|
||||
// assert_eq!(index.variables(), vec![]);
|
||||
// assert_eq!(index.fetched_ids, HashSet::default());
|
||||
|
||||
// let variable1 = VariableContainer {
|
||||
// variable: Variable {
|
||||
// name: "First variable".into(),
|
||||
// value: "First variable".into(),
|
||||
// type_: None,
|
||||
// presentation_hint: None,
|
||||
// evaluate_name: None,
|
||||
// variables_reference: 0,
|
||||
// named_variables: None,
|
||||
// indexed_variables: None,
|
||||
// memory_reference: None,
|
||||
// },
|
||||
// depth: 1,
|
||||
// container_reference: 1,
|
||||
// };
|
||||
|
||||
// let variable2 = VariableContainer {
|
||||
// variable: Variable {
|
||||
// name: "Second variable with child".into(),
|
||||
// value: "Second variable with child".into(),
|
||||
// type_: None,
|
||||
// presentation_hint: None,
|
||||
// evaluate_name: None,
|
||||
// variables_reference: 2,
|
||||
// named_variables: None,
|
||||
// indexed_variables: None,
|
||||
// memory_reference: None,
|
||||
// },
|
||||
// depth: 1,
|
||||
// container_reference: 1,
|
||||
// };
|
||||
|
||||
// let variable3 = VariableContainer {
|
||||
// variable: Variable {
|
||||
// name: "Third variable".into(),
|
||||
// value: "Third variable".into(),
|
||||
// type_: None,
|
||||
// presentation_hint: None,
|
||||
// evaluate_name: None,
|
||||
// variables_reference: 0,
|
||||
// named_variables: None,
|
||||
// indexed_variables: None,
|
||||
// memory_reference: None,
|
||||
// },
|
||||
// depth: 1,
|
||||
// container_reference: 1,
|
||||
// };
|
||||
|
||||
// index.add_variables(
|
||||
// 1,
|
||||
// vec![variable1.clone(), variable2.clone(), variable3.clone()],
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// vec![variable1.clone(), variable2.clone(), variable3.clone()],
|
||||
// index.variables(),
|
||||
// );
|
||||
// assert_eq!(HashSet::from([1]), index.fetched_ids,);
|
||||
}
|
||||
|
||||
/// This covers when you click on a variable that has a nested variable
|
||||
/// We correctly insert the variables right after the variable you clicked on
|
||||
#[test]
|
||||
fn test_add_sub_variables_to_index() {
|
||||
unimplemented!("This test hasn't been refactored yet")
|
||||
// let mut index = ScopeVariableIndex::new();
|
||||
|
||||
// assert_eq!(index.variables(), vec![]);
|
||||
|
||||
// let variable1 = VariableContainer {
|
||||
// variable: Variable {
|
||||
// name: "First variable".into(),
|
||||
// value: "First variable".into(),
|
||||
// type_: None,
|
||||
// presentation_hint: None,
|
||||
// evaluate_name: None,
|
||||
// variables_reference: 0,
|
||||
// named_variables: None,
|
||||
// indexed_variables: None,
|
||||
// memory_reference: None,
|
||||
// },
|
||||
// depth: 1,
|
||||
// container_reference: 1,
|
||||
// };
|
||||
|
||||
// let variable2 = VariableContainer {
|
||||
// variable: Variable {
|
||||
// name: "Second variable with child".into(),
|
||||
// value: "Second variable with child".into(),
|
||||
// type_: None,
|
||||
// presentation_hint: None,
|
||||
// evaluate_name: None,
|
||||
// variables_reference: 2,
|
||||
// named_variables: None,
|
||||
// indexed_variables: None,
|
||||
// memory_reference: None,
|
||||
// },
|
||||
// depth: 1,
|
||||
// container_reference: 1,
|
||||
// };
|
||||
|
||||
// let variable3 = VariableContainer {
|
||||
// variable: Variable {
|
||||
// name: "Third variable".into(),
|
||||
// value: "Third variable".into(),
|
||||
// type_: None,
|
||||
// presentation_hint: None,
|
||||
// evaluate_name: None,
|
||||
// variables_reference: 0,
|
||||
// named_variables: None,
|
||||
// indexed_variables: None,
|
||||
// memory_reference: None,
|
||||
// },
|
||||
// depth: 1,
|
||||
// container_reference: 1,
|
||||
// };
|
||||
|
||||
// index.add_variables(
|
||||
// 1,
|
||||
// vec![variable1.clone(), variable2.clone(), variable3.clone()],
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// vec![variable1.clone(), variable2.clone(), variable3.clone()],
|
||||
// index.variables(),
|
||||
// );
|
||||
// assert_eq!(HashSet::from([1]), index.fetched_ids);
|
||||
|
||||
// let variable4 = VariableContainer {
|
||||
// variable: Variable {
|
||||
// name: "Fourth variable".into(),
|
||||
// value: "Fourth variable".into(),
|
||||
// type_: None,
|
||||
// presentation_hint: None,
|
||||
// evaluate_name: None,
|
||||
// variables_reference: 0,
|
||||
// named_variables: None,
|
||||
// indexed_variables: None,
|
||||
// memory_reference: None,
|
||||
// },
|
||||
// depth: 1,
|
||||
// container_reference: 1,
|
||||
// };
|
||||
|
||||
// let variable5 = VariableContainer {
|
||||
// variable: Variable {
|
||||
// name: "Five variable".into(),
|
||||
// value: "Five variable".into(),
|
||||
// type_: None,
|
||||
// presentation_hint: None,
|
||||
// evaluate_name: None,
|
||||
// variables_reference: 0,
|
||||
// named_variables: None,
|
||||
// indexed_variables: None,
|
||||
// memory_reference: None,
|
||||
// },
|
||||
// depth: 1,
|
||||
// container_reference: 1,
|
||||
// };
|
||||
|
||||
// index.add_variables(2, vec![variable4.clone(), variable5.clone()]);
|
||||
|
||||
// assert_eq!(
|
||||
// vec![
|
||||
// variable1.clone(),
|
||||
// variable2.clone(),
|
||||
// variable4.clone(),
|
||||
// variable5.clone(),
|
||||
// variable3.clone(),
|
||||
// ],
|
||||
// index.variables(),
|
||||
// );
|
||||
// assert_eq!(index.fetched_ids, HashSet::from([1, 2]));
|
||||
}
|
||||
}
|
||||
65
crates/debugger_ui/src/session/starting.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use gpui::{
|
||||
percentage, Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task,
|
||||
Transformation,
|
||||
};
|
||||
use project::debugger::session::Session;
|
||||
use ui::{v_flex, Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled};
|
||||
|
||||
pub(super) struct StartingState {
|
||||
focus_handle: FocusHandle,
|
||||
_notify_parent: Task<()>,
|
||||
}
|
||||
|
||||
pub(crate) enum StartingEvent {
|
||||
Finished(Result<Entity<Session>>),
|
||||
}
|
||||
|
||||
impl EventEmitter<StartingEvent> for StartingState {}
|
||||
|
||||
impl StartingState {
|
||||
pub(crate) fn new(task: Task<Result<Entity<Session>>>, cx: &mut Context<Self>) -> Self {
|
||||
let _notify_parent = cx.spawn(move |this, mut cx| async move {
|
||||
let entity = task.await;
|
||||
this.update(&mut cx, |_, cx| cx.emit(StartingEvent::Finished(entity)))
|
||||
.ok();
|
||||
});
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
_notify_parent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for StartingState {
|
||||
fn focus_handle(&self, _: &ui::App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for StartingState {
|
||||
fn render(
|
||||
&mut self,
|
||||
_window: &mut ui::Window,
|
||||
_cx: &mut ui::Context<'_, Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child("Starting a debug adapter")
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Info)
|
||||
.with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(2)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
81
crates/debugger_ui/src/tests.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use gpui::{Entity, TestAppContext, WindowHandle};
|
||||
use project::{Project, Worktree};
|
||||
use settings::SettingsStore;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{debugger_panel::DebugPanel, session::DebugSession};
|
||||
|
||||
mod attach_modal;
|
||||
mod console;
|
||||
mod debugger_panel;
|
||||
mod module_list;
|
||||
mod stack_frame_list;
|
||||
mod variable_list;
|
||||
|
||||
pub fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
if std::env::var("RUST_LOG").is_ok() {
|
||||
env_logger::try_init().ok();
|
||||
}
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings = SettingsStore::test(cx);
|
||||
cx.set_global(settings);
|
||||
terminal_view::init(cx);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
command_palette_hooks::init(cx);
|
||||
language::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
editor::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn init_test_workspace(
|
||||
project: &Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> WindowHandle<Workspace> {
|
||||
let workspace_handle =
|
||||
cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let debugger_panel = workspace_handle
|
||||
.update(cx, |_, window, cx| cx.spawn_in(window, DebugPanel::load))
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("Failed to load debug panel");
|
||||
|
||||
let terminal_panel = workspace_handle
|
||||
.update(cx, |_, window, cx| cx.spawn_in(window, TerminalPanel::load))
|
||||
.unwrap()
|
||||
.await
|
||||
.expect("Failed to load terminal panel");
|
||||
|
||||
workspace_handle
|
||||
.update(cx, |workspace, window, cx| {
|
||||
workspace.add_panel(debugger_panel, window, cx);
|
||||
workspace.add_panel(terminal_panel, window, cx);
|
||||
})
|
||||
.unwrap();
|
||||
workspace_handle
|
||||
}
|
||||
|
||||
pub fn active_debug_panel_item(
|
||||
workspace: WindowHandle<Workspace>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<DebugSession> {
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
debug_panel
|
||||
.update(cx, |this, cx| this.active_debug_panel_item(cx))
|
||||
.unwrap()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn worktree_from_project(
|
||||
project: &Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<Worktree> {
|
||||
project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap())
|
||||
}
|
||||
277
crates/debugger_ui/src/tests/attach_modal.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use crate::*;
|
||||
use attach_modal::_AttachModal;
|
||||
use dap::requests::{Attach, Disconnect, Initialize};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use menu::{Cancel, Confirm};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use task::AttachConfig;
|
||||
use tests::{init_test, init_test_workspace};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let send_attach_request = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client
|
||||
.on_request::<Attach, _>({
|
||||
let send_attach_request = send_attach_request.clone();
|
||||
move |_, args| {
|
||||
send_attach_request.store(true, Ordering::SeqCst);
|
||||
|
||||
assert_eq!(json!({"request": "attach", "process_id": 10}), args.raw);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
send_attach_request.load(std::sync::atomic::Ordering::SeqCst),
|
||||
"Expected to send attach request, because we passed in the processId"
|
||||
);
|
||||
|
||||
// assert we didn't show the attach modal
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
assert!(workspace.active_modal::<_AttachModal>(cx).is_none());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_show_attach_modal_and_select_process(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let send_attach_request = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client
|
||||
.on_request::<Attach, _>({
|
||||
let send_attach_request = send_attach_request.clone();
|
||||
move |_, args| {
|
||||
send_attach_request.store(true, Ordering::SeqCst);
|
||||
|
||||
assert_eq!(
|
||||
json!({
|
||||
"request": "attach",
|
||||
// note we filtered out all processes in FakeAdapter::attach_processes,
|
||||
// that is not equal to the current process id
|
||||
"process_id": std::process::id(),
|
||||
}),
|
||||
args.raw
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// assert we show the attach modal
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let attach_modal = workspace.active_modal::<_AttachModal>(cx).unwrap();
|
||||
|
||||
let names = attach_modal.update(cx, |modal, cx| attach_modal::procss_names(&modal, cx));
|
||||
|
||||
// we filtered out all processes that are not the current process(zed itself)
|
||||
assert_eq!(1, names.len());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// select the only existing process
|
||||
cx.dispatch_action(Confirm);
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// assert attach modal was dismissed
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
assert!(workspace.active_modal::<_AttachModal>(cx).is_none());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
send_attach_request.load(std::sync::atomic::Ordering::SeqCst),
|
||||
"Expected to send attach request, because we passed in the processId"
|
||||
);
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_shutdown_session_when_modal_is_dismissed(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let send_attach_request = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client
|
||||
.on_request::<Attach, _>({
|
||||
let send_attach_request = send_attach_request.clone();
|
||||
move |_, _| {
|
||||
send_attach_request.store(true, Ordering::SeqCst);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// assert we show the attach modal
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let attach_modal = workspace.active_modal::<_AttachModal>(cx).unwrap();
|
||||
|
||||
let names = attach_modal.update(cx, |modal, cx| attach_modal::procss_names(&modal, cx));
|
||||
|
||||
// we filtered out all processes that are not the current process(zed itself)
|
||||
assert_eq!(1, names.len());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// close the modal
|
||||
cx.dispatch_action(Cancel);
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// assert attach modal was dismissed
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
assert!(workspace.active_modal::<_AttachModal>(cx).is_none());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
!send_attach_request.load(std::sync::atomic::Ordering::SeqCst),
|
||||
"Didn't expected to send attach request, because we closed the modal"
|
||||
);
|
||||
|
||||
// assert debug session is shutdown
|
||||
project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
assert!(dap_store.session_by_id(&session.read(cx).id()).is_none())
|
||||
});
|
||||
});
|
||||
}
|
||||
853
crates/debugger_ui/src/tests/console.rs
Normal file
@@ -0,0 +1,853 @@
|
||||
use crate::*;
|
||||
use dap::{
|
||||
requests::{Disconnect, Evaluate, Initialize, Launch, Scopes, StackTrace, Variables},
|
||||
Scope, StackFrame, Variable,
|
||||
};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
};
|
||||
use tests::{active_debug_panel_item, init_test, init_test_workspace};
|
||||
use unindent::Unindent as _;
|
||||
use variable_list::{VariableContainer, VariableListEntry};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Launch, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.on_request::<StackTrace, _>(move |_, _| {
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: Vec::default(),
|
||||
total_frames: None,
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: None,
|
||||
output: "First console output line before thread stopped!".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "First output line before thread stopped!".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// assert we have output from before the thread stopped
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
let active_debug_panel_item = debug_panel
|
||||
.update(cx, |this, cx| this.active_debug_panel_item(cx))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(1, debug_panel.read(cx).message_queue().len());
|
||||
|
||||
assert_eq!(
|
||||
"First console output line before thread stopped!\nFirst output line before thread stopped!\n",
|
||||
active_debug_panel_item.read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "Second output line after thread stopped!".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Console),
|
||||
output: "Second console output line after thread stopped!".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// assert we have output from before and after the thread stopped
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
let active_debug_panel_item = debug_panel
|
||||
.update(cx, |this, cx| this.active_debug_panel_item(cx))
|
||||
.unwrap();
|
||||
|
||||
assert!(!debug_panel.read(cx).message_queue().is_empty());
|
||||
|
||||
assert_eq!(
|
||||
"First console output line before thread stopped!\nFirst output line before thread stopped!\nSecond output line after thread stopped!\nSecond console output line after thread stopped!\n",
|
||||
active_debug_panel_item.read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"main.rs": "First line\nSecond line\nThird line\nFourth line",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Launch, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.on_request::<StackTrace, _>(move |_, _| {
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: Vec::default(),
|
||||
total_frames: None,
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: None,
|
||||
output: "First line".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "First group".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: Some(dap::OutputEventGroup::Start),
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "First item in group 1".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "Second item in group 1".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "Second group".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: Some(dap::OutputEventGroup::Start),
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "First item in group 2".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "Second item in group 2".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "End group 2".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: Some(dap::OutputEventGroup::End),
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "Third group".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: Some(dap::OutputEventGroup::StartCollapsed),
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "First item in group 3".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "Second item in group 3".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "End group 3".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: Some(dap::OutputEventGroup::End),
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "Third item in group 1".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
|
||||
category: Some(dap::OutputEventCategory::Stdout),
|
||||
output: "Second item".to_string(),
|
||||
data: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
group: Some(dap::OutputEventGroup::End),
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item.console().update(cx, |console, cx| {
|
||||
console.editor().update(cx, |editor, cx| {
|
||||
pretty_assertions::assert_eq!(
|
||||
"
|
||||
First line
|
||||
First group
|
||||
First item in group 1
|
||||
Second item in group 1
|
||||
Second group
|
||||
First item in group 2
|
||||
Second item in group 2
|
||||
End group 2
|
||||
⋯ End group 3
|
||||
Third item in group 1
|
||||
Second item
|
||||
"
|
||||
.unindent(),
|
||||
editor.display_text(cx)
|
||||
);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
const NEW_VALUE: &str = "{nested1: \"Nested 1 updated\", nested2: \"Nested 2 updated\"}";
|
||||
|
||||
let called_evaluate = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
let test_file_content = r#"
|
||||
const variable1 = {
|
||||
nested1: "Nested 1",
|
||||
nested2: "Nested 2",
|
||||
};
|
||||
const variable2 = "Value 2";
|
||||
const variable3 = "Value 3";
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"src": {
|
||||
"test.js": test_file_content,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Launch, _>(move |_, _| Ok(())).await;
|
||||
|
||||
let stack_frames = vec![StackFrame {
|
||||
id: 1,
|
||||
name: "Stack Frame 1".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("test.js".into()),
|
||||
path: Some("/project/src/test.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 3,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
}];
|
||||
|
||||
client
|
||||
.on_request::<StackTrace, _>({
|
||||
let stack_frames = Arc::new(stack_frames.clone());
|
||||
move |_, args| {
|
||||
assert_eq!(1, args.thread_id);
|
||||
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: (*stack_frames).clone(),
|
||||
total_frames: None,
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let scopes = vec![
|
||||
Scope {
|
||||
name: "Scope 1".into(),
|
||||
presentation_hint: None,
|
||||
variables_reference: 2,
|
||||
named_variables: None,
|
||||
indexed_variables: None,
|
||||
expensive: false,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
},
|
||||
Scope {
|
||||
name: "Scope 2".into(),
|
||||
presentation_hint: None,
|
||||
variables_reference: 4,
|
||||
named_variables: None,
|
||||
indexed_variables: None,
|
||||
expensive: false,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
},
|
||||
];
|
||||
|
||||
client
|
||||
.on_request::<Scopes, _>({
|
||||
let scopes = Arc::new(scopes.clone());
|
||||
move |_, args| {
|
||||
assert_eq!(1, args.frame_id);
|
||||
|
||||
Ok(dap::ScopesResponse {
|
||||
scopes: (*scopes).clone(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let scope1_variables = Arc::new(Mutex::new(vec![
|
||||
Variable {
|
||||
name: "variable1".into(),
|
||||
value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(),
|
||||
type_: None,
|
||||
presentation_hint: None,
|
||||
evaluate_name: None,
|
||||
variables_reference: 3,
|
||||
named_variables: None,
|
||||
indexed_variables: None,
|
||||
memory_reference: None,
|
||||
},
|
||||
Variable {
|
||||
name: "variable2".into(),
|
||||
value: "Value 2".into(),
|
||||
type_: None,
|
||||
presentation_hint: None,
|
||||
evaluate_name: None,
|
||||
variables_reference: 0,
|
||||
named_variables: None,
|
||||
indexed_variables: None,
|
||||
memory_reference: None,
|
||||
},
|
||||
]));
|
||||
|
||||
let nested_variables = vec![
|
||||
Variable {
|
||||
name: "nested1".into(),
|
||||
value: "Nested 1".into(),
|
||||
type_: None,
|
||||
presentation_hint: None,
|
||||
evaluate_name: None,
|
||||
variables_reference: 0,
|
||||
named_variables: None,
|
||||
indexed_variables: None,
|
||||
memory_reference: None,
|
||||
},
|
||||
Variable {
|
||||
name: "nested2".into(),
|
||||
value: "Nested 2".into(),
|
||||
type_: None,
|
||||
presentation_hint: None,
|
||||
evaluate_name: None,
|
||||
variables_reference: 0,
|
||||
named_variables: None,
|
||||
indexed_variables: None,
|
||||
memory_reference: None,
|
||||
},
|
||||
];
|
||||
|
||||
let scope2_variables = vec![Variable {
|
||||
name: "variable3".into(),
|
||||
value: "Value 3".into(),
|
||||
type_: None,
|
||||
presentation_hint: None,
|
||||
evaluate_name: None,
|
||||
variables_reference: 0,
|
||||
named_variables: None,
|
||||
indexed_variables: None,
|
||||
memory_reference: None,
|
||||
}];
|
||||
|
||||
client
|
||||
.on_request::<Variables, _>({
|
||||
let scope1_variables = scope1_variables.clone();
|
||||
let nested_variables = Arc::new(nested_variables.clone());
|
||||
let scope2_variables = Arc::new(scope2_variables.clone());
|
||||
move |_, args| match args.variables_reference {
|
||||
4 => Ok(dap::VariablesResponse {
|
||||
variables: (*scope2_variables).clone(),
|
||||
}),
|
||||
3 => Ok(dap::VariablesResponse {
|
||||
variables: (*nested_variables).clone(),
|
||||
}),
|
||||
2 => Ok(dap::VariablesResponse {
|
||||
variables: scope1_variables.lock().unwrap().clone(),
|
||||
}),
|
||||
id => unreachable!("unexpected variables reference {id}"),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client
|
||||
.on_request::<Evaluate, _>({
|
||||
let called_evaluate = called_evaluate.clone();
|
||||
let scope1_variables = scope1_variables.clone();
|
||||
move |_, args| {
|
||||
called_evaluate.store(true, Ordering::SeqCst);
|
||||
|
||||
assert_eq!(format!("$variable1 = {}", NEW_VALUE), args.expression);
|
||||
assert_eq!(Some(1), args.frame_id);
|
||||
assert_eq!(Some(dap::EvaluateArgumentsContext::Variables), args.context);
|
||||
|
||||
scope1_variables.lock().unwrap()[0].value = NEW_VALUE.to_string();
|
||||
|
||||
Ok(dap::EvaluateResponse {
|
||||
result: NEW_VALUE.into(),
|
||||
type_: None,
|
||||
presentation_hint: None,
|
||||
variables_reference: 0,
|
||||
named_variables: None,
|
||||
indexed_variables: None,
|
||||
memory_reference: None,
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// toggle nested variables for scope 1
|
||||
active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| {
|
||||
let scope1_variables = scope1_variables.lock().unwrap().clone();
|
||||
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, cx| {
|
||||
variable_list.toggle_entry(
|
||||
&variable_list::OpenEntry::Variable {
|
||||
scope_name: scopes[0].name.clone(),
|
||||
name: scope1_variables[0].name.clone(),
|
||||
depth: 1,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_panel_item(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
|
||||
debug_panel_item.console().update(cx, |console, item_cx| {
|
||||
console
|
||||
.query_bar()
|
||||
.update(item_cx, |query_bar, console_cx| {
|
||||
query_bar.set_text(format!("$variable1 = {}", NEW_VALUE), window, console_cx);
|
||||
});
|
||||
|
||||
console.evaluate(&menu::Confirm, window, item_cx);
|
||||
});
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| {
|
||||
assert_eq!(
|
||||
"",
|
||||
debug_panel_item
|
||||
.console()
|
||||
.read(cx)
|
||||
.query_bar()
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.as_str()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}\n", NEW_VALUE),
|
||||
debug_panel_item
|
||||
.console()
|
||||
.read(cx)
|
||||
.editor()
|
||||
.read(cx)
|
||||
.text(cx)
|
||||
.as_str()
|
||||
);
|
||||
|
||||
debug_panel_item
|
||||
.variable_list()
|
||||
.update(cx, |variable_list, _| {
|
||||
let scope1_variables = scope1_variables.lock().unwrap().clone();
|
||||
|
||||
// scope 1
|
||||
assert_eq!(
|
||||
vec![
|
||||
VariableContainer {
|
||||
container_reference: scopes[0].variables_reference,
|
||||
variable: scope1_variables[0].clone(),
|
||||
depth: 1,
|
||||
},
|
||||
VariableContainer {
|
||||
container_reference: scope1_variables[0].variables_reference,
|
||||
variable: nested_variables[0].clone(),
|
||||
depth: 2,
|
||||
},
|
||||
VariableContainer {
|
||||
container_reference: scope1_variables[0].variables_reference,
|
||||
variable: nested_variables[1].clone(),
|
||||
depth: 2,
|
||||
},
|
||||
VariableContainer {
|
||||
container_reference: scopes[0].variables_reference,
|
||||
variable: scope1_variables[1].clone(),
|
||||
depth: 1,
|
||||
},
|
||||
],
|
||||
variable_list.variables_by_scope(1, 2).unwrap().variables()
|
||||
);
|
||||
|
||||
// scope 2
|
||||
assert_eq!(
|
||||
vec![VariableContainer {
|
||||
container_reference: scopes[1].variables_reference,
|
||||
variable: scope2_variables[0].clone(),
|
||||
depth: 1,
|
||||
}],
|
||||
variable_list.variables_by_scope(1, 4).unwrap().variables()
|
||||
);
|
||||
|
||||
// assert visual entries
|
||||
assert_eq!(
|
||||
vec![
|
||||
VariableListEntry::Scope(scopes[0].clone()),
|
||||
VariableListEntry::Variable {
|
||||
depth: 1,
|
||||
scope: Arc::new(scopes[0].clone()),
|
||||
has_children: true,
|
||||
variable: Arc::new(scope1_variables[0].clone()),
|
||||
container_reference: scopes[0].variables_reference,
|
||||
},
|
||||
VariableListEntry::Variable {
|
||||
depth: 2,
|
||||
scope: Arc::new(scopes[0].clone()),
|
||||
has_children: false,
|
||||
variable: Arc::new(nested_variables[0].clone()),
|
||||
container_reference: scope1_variables[0].variables_reference,
|
||||
},
|
||||
VariableListEntry::Variable {
|
||||
depth: 2,
|
||||
scope: Arc::new(scopes[0].clone()),
|
||||
has_children: false,
|
||||
variable: Arc::new(nested_variables[1].clone()),
|
||||
container_reference: scope1_variables[0].variables_reference,
|
||||
},
|
||||
VariableListEntry::Variable {
|
||||
depth: 1,
|
||||
scope: Arc::new(scopes[0].clone()),
|
||||
has_children: false,
|
||||
variable: Arc::new(scope1_variables[1].clone()),
|
||||
container_reference: scopes[0].variables_reference,
|
||||
},
|
||||
VariableListEntry::Scope(scopes[1].clone()),
|
||||
],
|
||||
variable_list.entries().get(&1).unwrap().clone()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
assert!(
|
||||
called_evaluate.load(std::sync::atomic::Ordering::SeqCst),
|
||||
"Expected evaluate request to be called"
|
||||
);
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
1327
crates/debugger_ui/src/tests/debugger_panel.rs
Normal file
223
crates/debugger_ui/src/tests/module_list.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use crate::{
|
||||
session::ThreadItem,
|
||||
tests::{active_debug_panel_item, init_test, init_test_workspace},
|
||||
};
|
||||
use dap::{
|
||||
requests::{Disconnect, Initialize, Launch, Modules, StackTrace},
|
||||
StoppedEvent,
|
||||
};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_modules_request: Some(true),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Launch, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.on_request::<StackTrace, _>(move |_, _| {
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: Vec::default(),
|
||||
total_frames: None,
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
let called_modules = Arc::new(AtomicBool::new(false));
|
||||
let modules = vec![
|
||||
dap::Module {
|
||||
id: dap::ModuleId::Number(1),
|
||||
name: "First Module".into(),
|
||||
address_range: None,
|
||||
date_time_stamp: None,
|
||||
path: None,
|
||||
symbol_file_path: None,
|
||||
symbol_status: None,
|
||||
version: None,
|
||||
is_optimized: None,
|
||||
is_user_code: None,
|
||||
},
|
||||
dap::Module {
|
||||
id: dap::ModuleId::Number(2),
|
||||
name: "Second Module".into(),
|
||||
address_range: None,
|
||||
date_time_stamp: None,
|
||||
path: None,
|
||||
symbol_file_path: None,
|
||||
symbol_status: None,
|
||||
version: None,
|
||||
is_optimized: None,
|
||||
is_user_code: None,
|
||||
},
|
||||
];
|
||||
|
||||
client
|
||||
.on_request::<Modules, _>({
|
||||
let called_modules = called_modules.clone();
|
||||
let modules = modules.clone();
|
||||
move |_, _| unsafe {
|
||||
static mut REQUEST_COUNT: i32 = 1;
|
||||
assert_eq!(
|
||||
1, REQUEST_COUNT,
|
||||
"This request should only be called once from the host"
|
||||
);
|
||||
REQUEST_COUNT += 1;
|
||||
called_modules.store(true, Ordering::SeqCst);
|
||||
|
||||
Ok(dap::ModulesResponse {
|
||||
modules: modules.clone(),
|
||||
total_modules: Some(2u64),
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
!called_modules.load(std::sync::atomic::Ordering::SeqCst),
|
||||
"Request Modules shouldn't be called before it's needed"
|
||||
);
|
||||
|
||||
active_debug_panel_item(workspace, cx).update(cx, |item, cx| {
|
||||
item.set_thread_item(ThreadItem::Modules, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
assert!(
|
||||
called_modules.load(std::sync::atomic::Ordering::SeqCst),
|
||||
"Request Modules should be called because a user clicked on the module list"
|
||||
);
|
||||
|
||||
active_debug_panel_item(workspace, cx).update(cx, |item, cx| {
|
||||
item.set_thread_item(ThreadItem::Modules, cx);
|
||||
|
||||
let actual_modules = item.modules().update(cx, |list, cx| list.modules(cx));
|
||||
assert_eq!(modules, actual_modules);
|
||||
});
|
||||
|
||||
// Test all module events now
|
||||
// New Module
|
||||
// Changed
|
||||
// Removed
|
||||
|
||||
let new_module = dap::Module {
|
||||
id: dap::ModuleId::Number(3),
|
||||
name: "Third Module".into(),
|
||||
address_range: None,
|
||||
date_time_stamp: None,
|
||||
path: None,
|
||||
symbol_file_path: None,
|
||||
symbol_status: None,
|
||||
version: None,
|
||||
is_optimized: None,
|
||||
is_user_code: None,
|
||||
};
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
|
||||
reason: dap::ModuleEventReason::New,
|
||||
module: new_module.clone(),
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_panel_item(workspace, cx).update(cx, |item, cx| {
|
||||
let actual_modules = item.modules().update(cx, |list, cx| list.modules(cx));
|
||||
assert_eq!(actual_modules.len(), 3);
|
||||
assert!(actual_modules.contains(&new_module));
|
||||
});
|
||||
|
||||
let changed_module = dap::Module {
|
||||
id: dap::ModuleId::Number(2),
|
||||
name: "Modified Second Module".into(),
|
||||
address_range: None,
|
||||
date_time_stamp: None,
|
||||
path: None,
|
||||
symbol_file_path: None,
|
||||
symbol_status: None,
|
||||
version: None,
|
||||
is_optimized: None,
|
||||
is_user_code: None,
|
||||
};
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
|
||||
reason: dap::ModuleEventReason::Changed,
|
||||
module: changed_module.clone(),
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_panel_item(workspace, cx).update(cx, |item, cx| {
|
||||
let actual_modules = item.modules().update(cx, |list, cx| list.modules(cx));
|
||||
assert_eq!(actual_modules.len(), 3);
|
||||
assert!(actual_modules.contains(&changed_module));
|
||||
});
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
|
||||
reason: dap::ModuleEventReason::Removed,
|
||||
module: changed_module.clone(),
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_panel_item(workspace, cx).update(cx, |item, cx| {
|
||||
let actual_modules = item.modules().update(cx, |list, cx| list.modules(cx));
|
||||
assert_eq!(actual_modules.len(), 2);
|
||||
assert!(!actual_modules.contains(&changed_module));
|
||||
});
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
645
crates/debugger_ui/src/tests/stack_frame_list.rs
Normal file
@@ -0,0 +1,645 @@
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
stack_frame_list::StackFrameEntry,
|
||||
tests::{active_debug_panel_item, init_test, init_test_workspace},
|
||||
};
|
||||
use dap::{
|
||||
requests::{Disconnect, Initialize, Launch, StackTrace},
|
||||
StackFrame,
|
||||
};
|
||||
use editor::{Editor, ToPoint as _};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
use unindent::Unindent as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
let test_file_content = r#"
|
||||
import { SOME_VALUE } './module.js';
|
||||
|
||||
console.log(SOME_VALUE);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let module_file_content = r#"
|
||||
export SOME_VALUE = 'some value';
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"src": {
|
||||
"test.js": test_file_content,
|
||||
"module.js": module_file_content,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Launch, _>(move |_, _| Ok(())).await;
|
||||
|
||||
let stack_frames = vec![
|
||||
StackFrame {
|
||||
id: 1,
|
||||
name: "Stack Frame 1".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("test.js".into()),
|
||||
path: Some("/project/src/test.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 3,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
},
|
||||
StackFrame {
|
||||
id: 2,
|
||||
name: "Stack Frame 2".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("module.js".into()),
|
||||
path: Some("/project/src/module.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 1,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
},
|
||||
];
|
||||
|
||||
client
|
||||
.on_request::<StackTrace, _>({
|
||||
let stack_frames = Arc::new(stack_frames.clone());
|
||||
move |_, args| {
|
||||
assert_eq!(1, args.thread_id);
|
||||
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: (*stack_frames).clone(),
|
||||
total_frames: None,
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
let active_debug_panel_item = debug_panel
|
||||
.update(cx, |this, cx| this.active_debug_panel_item(cx))
|
||||
.unwrap();
|
||||
|
||||
active_debug_panel_item.update(cx, |debug_panel_item, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
debug_panel_item.stack_frame_list().update(cx, |list, cx| {
|
||||
(
|
||||
list.stack_frames(cx)
|
||||
.into_iter()
|
||||
.map(|frame| frame.dap)
|
||||
.collect::<Vec<_>>(),
|
||||
list.current_stack_frame_id(),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(1, stack_frame_id);
|
||||
assert_eq!(stack_frames, stack_frame_list);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
let test_file_content = r#"
|
||||
import { SOME_VALUE } './module.js';
|
||||
|
||||
console.log(SOME_VALUE);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let module_file_content = r#"
|
||||
export SOME_VALUE = 'some value';
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"src": {
|
||||
"test.js": test_file_content,
|
||||
"module.js": module_file_content,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Launch, _>(move |_, _| Ok(())).await;
|
||||
|
||||
let stack_frames = vec![
|
||||
StackFrame {
|
||||
id: 1,
|
||||
name: "Stack Frame 1".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("test.js".into()),
|
||||
path: Some("/project/src/test.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 3,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
},
|
||||
StackFrame {
|
||||
id: 2,
|
||||
name: "Stack Frame 2".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("module.js".into()),
|
||||
path: Some("/project/src/module.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 1,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
},
|
||||
];
|
||||
|
||||
client
|
||||
.on_request::<StackTrace, _>({
|
||||
let stack_frames = Arc::new(stack_frames.clone());
|
||||
move |_, args| {
|
||||
assert_eq!(1, args.thread_id);
|
||||
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: (*stack_frames).clone(),
|
||||
total_frames: None,
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
let active_debug_panel_item = debug_panel
|
||||
.update(cx, |this, cx| this.active_debug_panel_item(cx))
|
||||
.unwrap();
|
||||
|
||||
active_debug_panel_item.update(cx, |debug_panel_item, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
debug_panel_item.stack_frame_list().update(cx, |list, cx| {
|
||||
(
|
||||
list.stack_frames(cx)
|
||||
.into_iter()
|
||||
.map(|frame| frame.dap)
|
||||
.collect::<Vec<_>>(),
|
||||
list.current_stack_frame_id(),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(1, stack_frame_id);
|
||||
assert_eq!(stack_frames, stack_frame_list);
|
||||
});
|
||||
|
||||
let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
|
||||
assert_eq!(1, editors.len());
|
||||
|
||||
let project_path = editors[0]
|
||||
.update(cx, |editor, cx| editor.project_path(cx))
|
||||
.unwrap();
|
||||
assert_eq!("src/test.js", project_path.path.to_string_lossy());
|
||||
assert_eq!(test_file_content, editors[0].read(cx).text(cx));
|
||||
assert_eq!(
|
||||
vec![2..3],
|
||||
editors[0].update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
|
||||
editor
|
||||
.highlighted_rows::<editor::DebugCurrentRowHighlight>()
|
||||
.map(|(range, _)| {
|
||||
let start = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
start.row..end.row
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let stack_frame_list = workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
let active_debug_panel_item = debug_panel
|
||||
.update(cx, |this, cx| this.active_debug_panel_item(cx))
|
||||
.unwrap();
|
||||
|
||||
active_debug_panel_item.read(cx).stack_frame_list().clone()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// select second stack frame
|
||||
stack_frame_list
|
||||
.update_in(cx, |stack_frame_list, window, cx| {
|
||||
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
let active_debug_panel_item = debug_panel
|
||||
.update(cx, |this, cx| this.active_debug_panel_item(cx))
|
||||
.unwrap();
|
||||
|
||||
active_debug_panel_item.update(cx, |debug_panel_item, cx| {
|
||||
let (stack_frame_list, stack_frame_id) =
|
||||
debug_panel_item.stack_frame_list().update(cx, |list, cx| {
|
||||
(
|
||||
list.stack_frames(cx)
|
||||
.into_iter()
|
||||
.map(|frame| frame.dap)
|
||||
.collect::<Vec<_>>(),
|
||||
list.current_stack_frame_id(),
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(2, stack_frame_id);
|
||||
assert_eq!(stack_frames, stack_frame_list);
|
||||
});
|
||||
|
||||
let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
|
||||
assert_eq!(1, editors.len());
|
||||
|
||||
let project_path = editors[0]
|
||||
.update(cx, |editor, cx| editor.project_path(cx))
|
||||
.unwrap();
|
||||
assert_eq!("src/module.js", project_path.path.to_string_lossy());
|
||||
assert_eq!(module_file_content, editors[0].read(cx).text(cx));
|
||||
assert_eq!(
|
||||
vec![0..1],
|
||||
editors[0].update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
|
||||
editor
|
||||
.highlighted_rows::<editor::DebugCurrentRowHighlight>()
|
||||
.map(|(range, _)| {
|
||||
let start = range.start.to_point(&snapshot.buffer_snapshot);
|
||||
let end = range.end.to_point(&snapshot.buffer_snapshot);
|
||||
start.row..end.row
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
|
||||
let test_file_content = r#"
|
||||
import { SOME_VALUE } './module.js';
|
||||
|
||||
console.log(SOME_VALUE);
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let module_file_content = r#"
|
||||
export SOME_VALUE = 'some value';
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
"src": {
|
||||
"test.js": test_file_content,
|
||||
"module.js": module_file_content,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, ["/project".as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
let task = project.update(cx, |project, cx| {
|
||||
project.start_debug_session(dap::test_config(), cx)
|
||||
});
|
||||
|
||||
let (session, client) = task.await.unwrap();
|
||||
|
||||
client
|
||||
.on_request::<Initialize, _>(move |_, _| {
|
||||
Ok(dap::Capabilities {
|
||||
supports_step_back: Some(false),
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Launch, _>(move |_, _| Ok(())).await;
|
||||
|
||||
let stack_frames = vec![
|
||||
StackFrame {
|
||||
id: 1,
|
||||
name: "Stack Frame 1".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("test.js".into()),
|
||||
path: Some("/project/src/test.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 3,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
},
|
||||
StackFrame {
|
||||
id: 2,
|
||||
name: "Stack Frame 2".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("module.js".into()),
|
||||
path: Some("/project/src/module.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: Some("ignored".into()),
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 1,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
|
||||
},
|
||||
StackFrame {
|
||||
id: 3,
|
||||
name: "Stack Frame 3".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("module.js".into()),
|
||||
path: Some("/project/src/module.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: Some("ignored".into()),
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 1,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
|
||||
},
|
||||
StackFrame {
|
||||
id: 4,
|
||||
name: "Stack Frame 4".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("module.js".into()),
|
||||
path: Some("/project/src/module.js".into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 1,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: None,
|
||||
},
|
||||
];
|
||||
|
||||
client
|
||||
.on_request::<StackTrace, _>({
|
||||
let stack_frames = Arc::new(stack_frames.clone());
|
||||
move |_, args| {
|
||||
assert_eq!(1, args.thread_id);
|
||||
|
||||
Ok(dap::StackTraceResponse {
|
||||
stack_frames: (*stack_frames).clone(),
|
||||
total_frames: None,
|
||||
})
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
client.on_request::<Disconnect, _>(move |_, _| Ok(())).await;
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
|
||||
reason: dap::StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_panel_item(workspace, cx).update(cx, |debug_panel_item, cx| {
|
||||
debug_panel_item
|
||||
.stack_frame_list()
|
||||
.update(cx, |stack_frame_list, cx| {
|
||||
assert_eq!(
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames[0].clone()),
|
||||
StackFrameEntry::Collapsed(vec![
|
||||
stack_frames[1].clone(),
|
||||
stack_frames[2].clone()
|
||||
]),
|
||||
StackFrameEntry::Normal(stack_frames[3].clone()),
|
||||
],
|
||||
stack_frame_list.entries()
|
||||
);
|
||||
|
||||
stack_frame_list.expand_collapsed_entry(
|
||||
1,
|
||||
&vec![stack_frames[1].clone(), stack_frames[2].clone()],
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames[3].clone()),
|
||||
],
|
||||
stack_frame_list.entries()
|
||||
);
|
||||
});
|
||||
});
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
dap_store.shutdown_session(&session.read(cx).id(), cx)
|
||||
})
|
||||
});
|
||||
|
||||
shutdown_session.await.unwrap();
|
||||
}
|
||||
1701
crates/debugger_ui/src/tests/variable_list.rs
Normal file
@@ -55,6 +55,7 @@ linkify.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
@@ -395,6 +395,8 @@ gpui::actions!(
|
||||
SwitchSourceHeader,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleBreakpoint,
|
||||
EditLogBreakpoint,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
|
||||
@@ -811,6 +811,17 @@ impl DisplaySnapshot {
|
||||
.anchor_at(point.to_offset(self, bias), bias)
|
||||
}
|
||||
|
||||
pub fn display_point_to_breakpoint_anchor(&self, point: DisplayPoint) -> Anchor {
|
||||
let bias = if point.is_zero() {
|
||||
Bias::Right
|
||||
} else {
|
||||
Bias::Left
|
||||
};
|
||||
|
||||
self.buffer_snapshot
|
||||
.anchor_at(point.to_offset(self, bias), bias)
|
||||
}
|
||||
|
||||
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
|
||||
|
||||
@@ -82,14 +82,15 @@ use git::blame::GitBlame;
|
||||
use gpui::{
|
||||
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation,
|
||||
AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds,
|
||||
ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler,
|
||||
EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global,
|
||||
HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||
ClickEvent, ClipboardEntry, ClipboardItem, Context, DispatchPhase, Edges, Entity,
|
||||
EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight,
|
||||
Global, HighlightStyle, Hsla, KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad,
|
||||
ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, Task,
|
||||
TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle,
|
||||
WeakEntity, WeakFocusHandle, Window,
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
@@ -110,6 +111,11 @@ use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
use mouse_context_menu::MouseContextMenu;
|
||||
use persistence::DB;
|
||||
use project::{
|
||||
debugger::breakpoint_store::{BreakpointEditAction, BreakpointStore},
|
||||
ProjectPath,
|
||||
};
|
||||
|
||||
pub use proposed_changes_editor::{
|
||||
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||
};
|
||||
@@ -117,7 +123,6 @@ use smallvec::smallvec;
|
||||
use std::iter::Peekable;
|
||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||
|
||||
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
|
||||
pub use lsp::CompletionContext;
|
||||
use lsp::{
|
||||
CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat,
|
||||
@@ -134,7 +139,12 @@ use multi_buffer::{
|
||||
ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
|
||||
ToOffsetUtf16,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::{
|
||||
debugger::{
|
||||
breakpoint_store::{Breakpoint, BreakpointKind},
|
||||
dap_store::DapStore,
|
||||
},
|
||||
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Location, LocationLink,
|
||||
@@ -150,6 +160,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore};
|
||||
use smallvec::SmallVec;
|
||||
use snippet::Snippet;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
any::TypeId,
|
||||
borrow::Cow,
|
||||
@@ -160,7 +171,6 @@ use std::{
|
||||
ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use sum_tree::Bias;
|
||||
@@ -232,6 +242,7 @@ impl InlayId {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DebugCurrentRowHighlight {}
|
||||
enum DocumentHighlightRead {}
|
||||
enum DocumentHighlightWrite {}
|
||||
enum InputComposition {}
|
||||
@@ -551,6 +562,7 @@ struct ResolvedTasks {
|
||||
templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>,
|
||||
position: Anchor,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
|
||||
struct BufferOffset(usize);
|
||||
|
||||
@@ -715,6 +727,11 @@ pub struct Editor {
|
||||
expect_bounds_change: Option<Bounds<Pixels>>,
|
||||
tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>,
|
||||
tasks_update_task: Option<Task<()>>,
|
||||
pub breakpoint_store: Option<Entity<BreakpointStore>>,
|
||||
/// Allow's a user to create a breakpoint by selecting this indicator
|
||||
/// It should be None while a user is not hovering over the gutter
|
||||
/// Otherwise it represents the point that the breakpoint will be shown
|
||||
pub gutter_breakpoint_indicator: Option<DisplayPoint>,
|
||||
in_project_search: bool,
|
||||
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
|
||||
breadcrumb_header: Option<String>,
|
||||
@@ -1242,6 +1259,8 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let project::Event::ActiveDebugLineChanged = event {
|
||||
editor.go_to_active_debug_line(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
@@ -1283,6 +1302,11 @@ impl Editor {
|
||||
None
|
||||
};
|
||||
|
||||
let breakpoint_store = match (mode, project.as_ref()) {
|
||||
(EditorMode::Full, Some(project)) => Some(project.read(cx).breakpoint_store()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mut code_action_providers = Vec::new();
|
||||
let mut load_uncommitted_diff = None;
|
||||
if let Some(project) = project.clone() {
|
||||
@@ -1416,6 +1440,9 @@ impl Editor {
|
||||
blame: None,
|
||||
blame_subscription: None,
|
||||
tasks: Default::default(),
|
||||
|
||||
breakpoint_store,
|
||||
gutter_breakpoint_indicator: None,
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe_in(&buffer, window, Self::on_buffer_event),
|
||||
@@ -1465,6 +1492,8 @@ impl Editor {
|
||||
this.start_git_blame_inline(false, window, cx);
|
||||
}
|
||||
|
||||
this.go_to_active_debug_line(window, cx);
|
||||
|
||||
if let Some(buffer) = buffer.read(cx).as_singleton() {
|
||||
if let Some(project) = this.project.as_ref() {
|
||||
let handle = project.update(cx, |project, cx| {
|
||||
@@ -5589,14 +5618,28 @@ impl Editor {
|
||||
_style: &EditorStyle,
|
||||
row: DisplayRow,
|
||||
is_active: bool,
|
||||
breakpoint: Option<&(text::Anchor, Breakpoint)>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<IconButton> {
|
||||
let color = if breakpoint.is_some() {
|
||||
Color::Debugger
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
|
||||
let bp_kind = Arc::new(
|
||||
breakpoint
|
||||
.map(|(_, bp)| bp.kind.clone())
|
||||
.unwrap_or(BreakpointKind::Standard),
|
||||
);
|
||||
|
||||
if self.available_code_actions.is_some() {
|
||||
Some(
|
||||
IconButton::new("code_actions_indicator", ui::IconName::Bolt)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_color(color)
|
||||
.toggle_state(is_active)
|
||||
.tooltip({
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
@@ -5621,6 +5664,16 @@ impl Editor {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
position,
|
||||
bp_kind.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
@@ -5639,6 +5692,199 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all display points of breakpoints that will be rendered within editor
|
||||
///
|
||||
/// This function is used to handle overlaps between breakpoints and Code action/runner symbol.
|
||||
/// It's also used to set the color of line numbers with breakpoints to the breakpoint color.
|
||||
/// TODO debugger: Use this function to color toggle symbols that house nested breakpoints
|
||||
fn active_breakpoints(
|
||||
&mut self,
|
||||
range: Range<DisplayRow>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> HashMap<DisplayRow, (text::Anchor, Breakpoint)> {
|
||||
let mut breakpoint_display_points = HashMap::default();
|
||||
|
||||
let Some(breakpoint_store) = self.breakpoint_store.clone() else {
|
||||
return breakpoint_display_points;
|
||||
};
|
||||
|
||||
let snapshot = self.snapshot(window, cx);
|
||||
|
||||
let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot;
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return breakpoint_display_points;
|
||||
};
|
||||
|
||||
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
for breakpoint in
|
||||
breakpoint_store
|
||||
.read(cx)
|
||||
.breakpoints(&buffer, None, buffer_snapshot.clone(), cx)
|
||||
{
|
||||
let point = buffer_snapshot.summary_for_anchor::<Point>(&breakpoint.0);
|
||||
breakpoint_display_points.insert(
|
||||
snapshot
|
||||
.point_to_display_point(
|
||||
MultiBufferPoint {
|
||||
row: point.row,
|
||||
column: point.column,
|
||||
},
|
||||
Bias::Left,
|
||||
)
|
||||
.row(),
|
||||
breakpoint.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
return breakpoint_display_points;
|
||||
}
|
||||
|
||||
let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left)
|
||||
..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
|
||||
for excerpt_boundary in multi_buffer_snapshot.excerpt_boundaries_in_range(range) {
|
||||
let info = excerpt_boundary.next.as_ref();
|
||||
|
||||
if let Some(info) = info {
|
||||
let Some(excerpt_ranges) = multi_buffer_snapshot.range_for_excerpt(info.id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(buffer) =
|
||||
project.read_with(cx, |this, cx| this.buffer_for_id(info.buffer_id, cx))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let breakpoints = breakpoint_store.read(cx).breakpoints(
|
||||
&buffer,
|
||||
Some(info.range.context.start..info.range.context.end),
|
||||
info.buffer.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
// To translate a breakpoint's position within a singular buffer to a multi buffer
|
||||
// position we need to know it's excerpt starting location, it's position within
|
||||
// the singular buffer, and if that position is within the excerpt's range.
|
||||
let excerpt_head = excerpt_ranges
|
||||
.start
|
||||
.to_display_point(&snapshot.display_snapshot);
|
||||
|
||||
let buffer_start = info
|
||||
.buffer
|
||||
.summary_for_anchor::<Point>(&info.range.context.start);
|
||||
|
||||
for (anchor, breakpoint) in breakpoints {
|
||||
let as_row = info.buffer.summary_for_anchor::<Point>(&anchor).row;
|
||||
let delta = as_row - buffer_start.row;
|
||||
|
||||
let position = excerpt_head + DisplayPoint::new(DisplayRow(delta), 0);
|
||||
|
||||
breakpoint_display_points
|
||||
.insert(position.row(), (anchor.clone(), breakpoint.clone()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
breakpoint_display_points
|
||||
}
|
||||
|
||||
fn breakpoint_context_menu(
|
||||
&self,
|
||||
anchor: text::Anchor,
|
||||
kind: Arc<BreakpointKind>,
|
||||
row: DisplayRow,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ui::ContextMenu> {
|
||||
let weak_editor = cx.weak_entity();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let second_entry_msg = if kind.log_message().is_some() {
|
||||
"Edit Log Breakpoint"
|
||||
} else {
|
||||
"Add Log Breakpoint"
|
||||
};
|
||||
|
||||
ui::ContextMenu::build(window, cx, |menu, _, _cx| {
|
||||
menu.on_blur_subscription(Subscription::new(|| {}))
|
||||
.context(focus_handle)
|
||||
.entry("Toggle Breakpoint", None, {
|
||||
let weak_editor = weak_editor.clone();
|
||||
move |_window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
BreakpointKind::Standard,
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
.entry(second_entry_msg, None, move |window, cx| {
|
||||
weak_editor
|
||||
.update(cx, |this, cx| {
|
||||
this.add_edit_breakpoint_block(row, anchor, kind.as_ref(), window, cx);
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn render_breakpoint(
|
||||
&self,
|
||||
position: text::Anchor,
|
||||
row: DisplayRow,
|
||||
kind: &BreakpointKind,
|
||||
cx: &mut Context<Self>,
|
||||
) -> IconButton {
|
||||
let color = if self
|
||||
.gutter_breakpoint_indicator
|
||||
.is_some_and(|gutter_bp| gutter_bp.row() == row)
|
||||
{
|
||||
Color::Hint
|
||||
} else {
|
||||
Color::Debugger
|
||||
};
|
||||
|
||||
let icon = match &kind {
|
||||
BreakpointKind::Standard => ui::IconName::DebugBreakpoint,
|
||||
BreakpointKind::Log(_) => ui::IconName::DebugLogBreakpoint,
|
||||
};
|
||||
let arc_kind = Arc::new(kind.clone());
|
||||
let arc_kind2 = arc_kind.clone();
|
||||
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.size(ui::ButtonSize::None)
|
||||
.icon_color(color)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener(move |editor, _e, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
position,
|
||||
arc_kind.as_ref().clone(),
|
||||
BreakpointEditAction::Toggle,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
Some(position),
|
||||
arc_kind2.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_tasks_context(
|
||||
project: &Entity<Project>,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -5775,12 +6021,26 @@ impl Editor {
|
||||
_style: &EditorStyle,
|
||||
is_active: bool,
|
||||
row: DisplayRow,
|
||||
breakpoint: Option<(text::Anchor, Breakpoint)>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> IconButton {
|
||||
let color = if breakpoint.is_some() {
|
||||
Color::Debugger
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let position = breakpoint.as_ref().map(|(anchor, _)| *anchor);
|
||||
let bp_kind = Arc::new(
|
||||
breakpoint
|
||||
.map(|(_, bp)| bp.kind)
|
||||
.unwrap_or(BreakpointKind::Standard),
|
||||
);
|
||||
|
||||
IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_color(color)
|
||||
.toggle_state(is_active)
|
||||
.on_click(cx.listener(move |editor, _e, window, cx| {
|
||||
window.focus(&editor.focus_handle(cx));
|
||||
@@ -5792,6 +6052,16 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
.on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| {
|
||||
editor.set_breakpoint_context_menu(
|
||||
row,
|
||||
position,
|
||||
bp_kind.clone(),
|
||||
event.down.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn context_menu_visible(&self) -> bool {
|
||||
@@ -7666,6 +7936,237 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_breakpoint_context_menu(
|
||||
&mut self,
|
||||
row: DisplayRow,
|
||||
position: Option<text::Anchor>,
|
||||
kind: Arc<BreakpointKind>,
|
||||
clicked_point: gpui::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let source = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.snapshot(cx)
|
||||
.breakpoint_anchor(Point::new(row.0, 0u32));
|
||||
|
||||
let context_menu = self.breakpoint_context_menu(
|
||||
position.unwrap_or(source.text_anchor),
|
||||
kind,
|
||||
row,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.mouse_context_menu = MouseContextMenu::pinned_to_editor(
|
||||
self,
|
||||
source,
|
||||
clicked_point,
|
||||
context_menu,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn add_edit_breakpoint_block(
|
||||
&mut self,
|
||||
row: DisplayRow,
|
||||
anchor: text::Anchor,
|
||||
kind: &BreakpointKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let position = self
|
||||
.snapshot(window, cx)
|
||||
.display_point_to_anchor(DisplayPoint::new(row, 0), Bias::Right);
|
||||
|
||||
let weak_editor = cx.weak_entity();
|
||||
let bp_prompt =
|
||||
cx.new(|cx| BreakpointPromptEditor::new(weak_editor, anchor, kind.clone(), window, cx));
|
||||
|
||||
let height = bp_prompt.update(cx, |this, cx| {
|
||||
this.prompt
|
||||
.update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2)
|
||||
});
|
||||
let cloned_prompt = bp_prompt.clone();
|
||||
let blocks = vec![BlockProperties {
|
||||
style: BlockStyle::Sticky,
|
||||
placement: BlockPlacement::Above(position),
|
||||
height,
|
||||
render: Arc::new(move |cx| {
|
||||
*cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions;
|
||||
cloned_prompt.clone().into_any_element()
|
||||
}),
|
||||
priority: 0,
|
||||
}];
|
||||
|
||||
let focus_handle = bp_prompt.focus_handle(cx);
|
||||
window.focus(&focus_handle);
|
||||
|
||||
let block_ids = self.insert_blocks(blocks, None, cx);
|
||||
bp_prompt.update(cx, |prompt, _| {
|
||||
prompt.add_block_ids(block_ids);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn breakpoint_at_cursor_head(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<(text::Anchor, Breakpoint)> {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
|
||||
// We Set the column position to zero so this function interacts correctly
|
||||
// between calls by clicking on the gutter & using an action to toggle a
|
||||
// breakpoint. Otherwise, toggling a breakpoint through an action wouldn't
|
||||
// untoggle a breakpoint that was added through clicking on the gutter
|
||||
let breakpoint_position = self
|
||||
.snapshot(window, cx)
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.breakpoint_anchor(Point::new(cursor_position.row, 0))
|
||||
.text_anchor;
|
||||
|
||||
let project = self.project.clone();
|
||||
|
||||
let buffer_id = breakpoint_position.buffer_id?;
|
||||
let buffer = project?.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?;
|
||||
let (buffer_snapshot, project_path) = (
|
||||
buffer.read(cx).snapshot(),
|
||||
buffer.read(cx).project_path(cx)?,
|
||||
);
|
||||
|
||||
let row = buffer_snapshot
|
||||
.summary_for_anchor::<Point>(&breakpoint_position)
|
||||
.row;
|
||||
|
||||
let bp = self
|
||||
.breakpoint_store
|
||||
.as_ref()?
|
||||
.read_with(cx, |breakpoint_store, cx| {
|
||||
breakpoint_store
|
||||
.breakpoints(
|
||||
&buffer,
|
||||
Some(breakpoint_position..(text::Anchor::MAX)),
|
||||
buffer_snapshot.clone(),
|
||||
cx,
|
||||
)
|
||||
.next()
|
||||
.filter(|(anchor, _)| {
|
||||
buffer_snapshot.summary_for_anchor::<Point>(anchor).row == row
|
||||
})
|
||||
.cloned()
|
||||
});
|
||||
bp
|
||||
}
|
||||
|
||||
pub fn edit_log_breakpoint(
|
||||
&mut self,
|
||||
_: &EditLogBreakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let (anchor, bp) = self
|
||||
.breakpoint_at_cursor_head(window, cx)
|
||||
.unwrap_or_else(|| {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
|
||||
let breakpoint_position = self
|
||||
.snapshot(window, cx)
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.breakpoint_anchor(Point::new(cursor_position.row, 0))
|
||||
.text_anchor;
|
||||
|
||||
(
|
||||
breakpoint_position,
|
||||
Breakpoint {
|
||||
kind: BreakpointKind::Standard,
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(buffer) = self
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.map(|buffer| buffer.read(cx))
|
||||
{
|
||||
let row = buffer
|
||||
.summary_for_anchor::<Point>(&anchor)
|
||||
.to_display_point(&self.snapshot(window, cx))
|
||||
.row();
|
||||
|
||||
self.add_edit_breakpoint_block(row, anchor, &bp.kind, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_breakpoint(
|
||||
&mut self,
|
||||
_: &ToggleBreakpoint,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let edit_action = BreakpointEditAction::Toggle;
|
||||
|
||||
if let Some((anchor, breakpoint)) = self.breakpoint_at_cursor_head(window, cx) {
|
||||
self.edit_breakpoint_at_anchor(anchor, breakpoint.kind, edit_action, cx);
|
||||
} else {
|
||||
let cursor_position: Point = self.selections.newest(cx).head();
|
||||
|
||||
let breakpoint_position = self
|
||||
.snapshot(window, cx)
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.breakpoint_anchor(Point::new(cursor_position.row, 0))
|
||||
.text_anchor;
|
||||
|
||||
self.edit_breakpoint_at_anchor(
|
||||
breakpoint_position,
|
||||
BreakpointKind::Standard,
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_breakpoint_at_anchor(
|
||||
&mut self,
|
||||
breakpoint_position: text::Anchor,
|
||||
kind: BreakpointKind,
|
||||
edit_action: BreakpointEditAction,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(breakpoint_store) = &self.breakpoint_store else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(buffer_id) = breakpoint_position.buffer_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
breakpoint_store.update(cx, |breakpoint_store, cx| {
|
||||
breakpoint_store.toggle_breakpoint(
|
||||
buffer.into(),
|
||||
(breakpoint_position, Breakpoint { kind }),
|
||||
edit_action,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn breakpoint_store(&self) -> Option<Entity<BreakpointStore>> {
|
||||
self.breakpoint_store.clone()
|
||||
}
|
||||
|
||||
pub fn prepare_restore_change(
|
||||
&self,
|
||||
revert_changes: &mut HashMap<BufferId, Vec<(Range<text::Anchor>, Rope)>>,
|
||||
@@ -11350,6 +11851,33 @@ impl Editor {
|
||||
hunk
|
||||
}
|
||||
|
||||
fn go_to_line<T: 'static>(
|
||||
&mut self,
|
||||
position: Anchor,
|
||||
highlight_color: Option<Hsla>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let snapshot = self.snapshot(window, cx).display_snapshot;
|
||||
let position = position.to_point(&snapshot.buffer_snapshot);
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.clip_point(Point::new(position.row, 0), Bias::Left);
|
||||
let end = start + Point::new(1, 0);
|
||||
let start = snapshot.buffer_snapshot.anchor_before(start);
|
||||
let end = snapshot.buffer_snapshot.anchor_before(end);
|
||||
|
||||
self.clear_row_highlights::<T>();
|
||||
self.highlight_rows::<T>(
|
||||
start..end,
|
||||
highlight_color
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.request_autoscroll(Autoscroll::center(), cx);
|
||||
}
|
||||
|
||||
pub fn go_to_definition(
|
||||
&mut self,
|
||||
_: &GoToDefinition,
|
||||
@@ -14056,6 +14584,55 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_path(&self, cx: &mut Context<Self>) -> Option<ProjectPath> {
|
||||
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
||||
buffer.read_with(cx, |buffer, cx| buffer.project_path(cx))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let _ = maybe!({
|
||||
let breakpoint_store = self.breakpoint_store.as_ref()?;
|
||||
|
||||
let (path, active_position) = breakpoint_store.read(cx).active_position().cloned()?;
|
||||
let snapshot = self
|
||||
.project
|
||||
.as_ref()?
|
||||
.read(cx)
|
||||
.buffer_for_id(active_position.buffer_id?, cx)?
|
||||
.read(cx)
|
||||
.snapshot();
|
||||
|
||||
for (id, ExcerptRange { context, .. }) in self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.excerpts_for_buffer(active_position.buffer_id?, cx)
|
||||
{
|
||||
if context.start.cmp(&active_position, &snapshot).is_ge()
|
||||
|| context.end.cmp(&active_position, &snapshot).is_lt()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let multibuffer_anchor = snapshot.anchor_in_excerpt(id, active_position)?;
|
||||
|
||||
self.clear_row_highlights::<DebugCurrentRowHighlight>();
|
||||
self.go_to_line::<DebugCurrentRowHighlight>(
|
||||
multibuffer_anchor,
|
||||
Some(cx.theme().colors().editor_debugger_active_line_background),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
pub fn copy_file_name_without_extension(
|
||||
&mut self,
|
||||
_: &CopyFileNameWithoutExtension,
|
||||
@@ -15123,9 +15700,10 @@ impl Editor {
|
||||
}
|
||||
multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
|
||||
multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
|
||||
multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
|
||||
cx.emit(EditorEvent::TitleChanged)
|
||||
multi_buffer::Event::FileHandleChanged => {
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
}
|
||||
multi_buffer::Event::Reloaded => cx.emit(EditorEvent::TitleChanged),
|
||||
// multi_buffer::Event::DiffBaseChanged => {
|
||||
// self.scrollbar_marker_state.dirty = true;
|
||||
// cx.emit(EditorEvent::DiffBaseChanged);
|
||||
@@ -17973,6 +18551,158 @@ impl Global for KillRing {}
|
||||
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
struct BreakpointPromptEditor {
|
||||
pub(crate) prompt: Entity<Editor>,
|
||||
editor: WeakEntity<Editor>,
|
||||
breakpoint_anchor: text::Anchor,
|
||||
kind: BreakpointKind,
|
||||
block_ids: HashSet<CustomBlockId>,
|
||||
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl BreakpointPromptEditor {
|
||||
const MAX_LINES: u8 = 4;
|
||||
|
||||
fn new(
|
||||
editor: WeakEntity<Editor>,
|
||||
breakpoint_anchor: text::Anchor,
|
||||
kind: BreakpointKind,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let buffer = cx.new(|cx| {
|
||||
Buffer::local(
|
||||
kind.log_message()
|
||||
.map(|msg| msg.to_string())
|
||||
.unwrap_or_default(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
|
||||
let prompt = cx.new(|cx| {
|
||||
let mut prompt = Editor::new(
|
||||
EditorMode::AutoHeight {
|
||||
max_lines: Self::MAX_LINES as usize,
|
||||
},
|
||||
buffer,
|
||||
None,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
|
||||
prompt.set_show_cursor_when_unfocused(false, cx);
|
||||
prompt.set_placeholder_text(
|
||||
"Message to log when breakpoint is hit. Expressions within {} are interpolated.",
|
||||
cx,
|
||||
);
|
||||
|
||||
prompt
|
||||
});
|
||||
|
||||
Self {
|
||||
prompt,
|
||||
editor,
|
||||
breakpoint_anchor,
|
||||
kind,
|
||||
gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())),
|
||||
block_ids: Default::default(),
|
||||
_subscriptions: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_block_ids(&mut self, block_ids: Vec<CustomBlockId>) {
|
||||
self.block_ids.extend(block_ids)
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(editor) = self.editor.upgrade() {
|
||||
let log_message = self
|
||||
.prompt
|
||||
.read(cx)
|
||||
.buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.expect("A multi buffer in breakpoint prompt isn't possible")
|
||||
.read(cx)
|
||||
.as_rope()
|
||||
.to_string();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
self.breakpoint_anchor,
|
||||
self.kind.clone(),
|
||||
BreakpointEditAction::EditLogMessage(log_message.into()),
|
||||
cx,
|
||||
);
|
||||
|
||||
editor.remove_blocks(self.block_ids.clone(), None, cx);
|
||||
cx.focus_self(window);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(self.block_ids.clone(), None, cx);
|
||||
window.focus(&editor.focus_handle);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_prompt_editor(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if self.prompt.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.buffer_font.family.clone(),
|
||||
font_fallbacks: settings.buffer_font.fallbacks.clone(),
|
||||
font_size: settings.buffer_font_size(cx).into(),
|
||||
font_weight: settings.buffer_font.weight,
|
||||
line_height: relative(settings.buffer_line_height.value()),
|
||||
..Default::default()
|
||||
};
|
||||
EditorElement::new(
|
||||
&self.prompt,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BreakpointPromptEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let gutter_dimensions = *self.gutter_dimensions.lock();
|
||||
h_flex()
|
||||
.key_context("Editor")
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().status().info_border)
|
||||
.size_full()
|
||||
.py(window.line_height() / 2.5)
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0)))
|
||||
.child(div().flex_1().child(self.render_prompt_editor(cx)))
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for BreakpointPromptEditor {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.prompt.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn all_edits_insertions_or_deletions(
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
|
||||
@@ -27,8 +27,11 @@ use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use multi_buffer::IndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
use project::FakeFs;
|
||||
use project::{
|
||||
debugger::breakpoint_store::BreakpointKind,
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
FakeFs,
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use std::{
|
||||
@@ -11196,6 +11199,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(after);
|
||||
};
|
||||
|
||||
@@ -16061,6 +16065,348 @@ async fn assert_highlighted_edits(
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_breakpoint(
|
||||
breakpoints: &BTreeMap<ProjectPath, collections::HashSet<Breakpoint>>,
|
||||
project_path: &ProjectPath,
|
||||
expected: Vec<(u32, BreakpointKind)>,
|
||||
) {
|
||||
if expected.len() == 0usize {
|
||||
assert!(!breakpoints.contains_key(project_path));
|
||||
} else {
|
||||
let mut breakpoint = breakpoints
|
||||
.get(project_path)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|breakpoint| (breakpoint.cached_position, breakpoint.kind.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
|
||||
|
||||
assert_eq!(expected, breakpoint);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_log_breakpoint_at_cursor(
|
||||
editor: &mut Editor,
|
||||
log_message: &str,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let (anchor, kind) = editor
|
||||
.breakpoint_at_cursor_head(window, cx)
|
||||
.unwrap_or_else(|| {
|
||||
let cursor_position: Point = editor.selections.newest(cx).head();
|
||||
|
||||
let breakpoint_position = editor
|
||||
.snapshot(window, cx)
|
||||
.display_snapshot
|
||||
.buffer_snapshot
|
||||
.breakpoint_anchor(Point::new(cursor_position.row, 0))
|
||||
.text_anchor;
|
||||
|
||||
let kind = BreakpointKind::Standard;
|
||||
|
||||
(breakpoint_position, kind)
|
||||
});
|
||||
|
||||
editor.edit_breakpoint_at_anchor(
|
||||
anchor,
|
||||
kind,
|
||||
BreakpointEditAction::EditLogMessage(log_message.into()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": sample_text,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": sample_text,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
|
||||
let worktree_id = workspace
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::Full,
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
|
||||
|
||||
// assert we can add breakpoint on the first line
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(1, breakpoints.len());
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&project_path,
|
||||
vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.move_to_beginning(&MoveToBeginning, window, cx);
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(1, breakpoints.len());
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&project_path,
|
||||
vec![(3, BreakpointKind::Standard)],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_eq!(0, breakpoints.len());
|
||||
assert_breakpoint(&breakpoints, &project_path, vec![]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": sample_text,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
let worktree_id = workspace.update(cx, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
})
|
||||
});
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer((worktree_id, "main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::Full,
|
||||
MultiBuffer::build_from_buffer(buffer, cx),
|
||||
Some(project),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&project_path,
|
||||
vec![(0, BreakpointKind::Log("hello world".into()))],
|
||||
);
|
||||
|
||||
// Removing a log message from a log breakpoint should remove it
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
add_log_breakpoint_at_cursor(editor, "", window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_breakpoint(&breakpoints, &project_path, vec![]);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
|
||||
// Not adding a log message to a standard breakpoint shouldn't remove it
|
||||
add_log_breakpoint_at_cursor(editor, "", window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&project_path,
|
||||
vec![(0, BreakpointKind::Standard), (3, BreakpointKind::Standard)],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&project_path,
|
||||
vec![
|
||||
(0, BreakpointKind::Standard),
|
||||
(3, BreakpointKind::Log("hello world".into())),
|
||||
],
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
|
||||
});
|
||||
|
||||
let breakpoints = editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.project
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.breakpoint_store()
|
||||
.read(cx)
|
||||
.breakpoints()
|
||||
.clone()
|
||||
});
|
||||
|
||||
assert_breakpoint(
|
||||
&breakpoints,
|
||||
&project_path,
|
||||
vec![
|
||||
(0, BreakpointKind::Standard),
|
||||
(3, BreakpointKind::Log("hello Earth !!".into())),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -52,7 +52,10 @@ use multi_buffer::{
|
||||
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
RowInfo,
|
||||
};
|
||||
use project::project_settings::{self, GitGutterSetting, ProjectSettings};
|
||||
use project::{
|
||||
debugger::breakpoint_store::{Breakpoint, BreakpointKind},
|
||||
project_settings::{self, GitGutterSetting, ProjectSettings},
|
||||
};
|
||||
use settings::Settings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use std::{
|
||||
@@ -61,6 +64,7 @@ use std::{
|
||||
cmp::{self, Ordering},
|
||||
fmt::{self, Write},
|
||||
iter, mem,
|
||||
num::NonZeroU32,
|
||||
ops::{Deref, Range},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
@@ -92,6 +96,14 @@ enum DisplayDiffHunk {
|
||||
},
|
||||
}
|
||||
|
||||
/// Determines what kinds of highlights should be applied to a lines background.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
struct LineHighlightSpec {
|
||||
selection: bool,
|
||||
breakpoint: bool,
|
||||
active_stack_frame: bool,
|
||||
}
|
||||
|
||||
struct SelectionLayout {
|
||||
head: DisplayPoint,
|
||||
cursor_shape: CursorShape,
|
||||
@@ -507,6 +519,8 @@ impl EditorElement {
|
||||
register_action(editor, window, Editor::insert_uuid_v4);
|
||||
register_action(editor, window, Editor::insert_uuid_v7);
|
||||
register_action(editor, window, Editor::open_selections_in_multibuffer);
|
||||
register_action(editor, window, Editor::toggle_breakpoint);
|
||||
register_action(editor, window, Editor::edit_log_breakpoint);
|
||||
}
|
||||
|
||||
fn register_key_listeners(&self, window: &mut Window, _: &mut App, layout: &EditorLayout) {
|
||||
@@ -854,6 +868,18 @@ impl EditorElement {
|
||||
let gutter_hovered = gutter_hitbox.is_hovered(window);
|
||||
editor.set_gutter_hovered(gutter_hovered, cx);
|
||||
|
||||
if gutter_hovered {
|
||||
editor.gutter_breakpoint_indicator = Some(
|
||||
position_map
|
||||
.point_for_position(event.position)
|
||||
.previous_valid,
|
||||
);
|
||||
} else {
|
||||
editor.gutter_breakpoint_indicator = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
|
||||
// Don't trigger hover popover if mouse is hovering over context menu
|
||||
if text_hitbox.is_hovered(window) {
|
||||
let point_for_position = position_map.point_for_position(event.position);
|
||||
@@ -950,7 +976,7 @@ impl EditorElement {
|
||||
cx: &mut App,
|
||||
) -> (
|
||||
Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
BTreeMap<DisplayRow, bool>,
|
||||
BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
Option<DisplayPoint>,
|
||||
) {
|
||||
let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
|
||||
@@ -980,9 +1006,10 @@ impl EditorElement {
|
||||
for row in cmp::max(layout.active_rows.start.0, start_row.0)
|
||||
..=cmp::min(layout.active_rows.end.0, end_row.0)
|
||||
{
|
||||
let contains_non_empty_selection =
|
||||
active_rows.entry(DisplayRow(row)).or_insert(!is_empty);
|
||||
*contains_non_empty_selection |= !is_empty;
|
||||
let contains_non_empty_selection = active_rows
|
||||
.entry(DisplayRow(row))
|
||||
.or_insert_with(LineHighlightSpec::default);
|
||||
contains_non_empty_selection.selection |= !is_empty;
|
||||
}
|
||||
layouts.push(layout);
|
||||
}
|
||||
@@ -2032,6 +2059,53 @@ impl EditorElement {
|
||||
(offset_y, length)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_breakpoints(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
range: Range<DisplayRow>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
breakpoints: HashMap<DisplayRow, (text::Anchor, Breakpoint)>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Vec<AnyElement> {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
breakpoints
|
||||
.into_iter()
|
||||
.filter_map(|(point, (text_anchor, bp))| {
|
||||
let row = MultiBufferRow { 0: point.0 };
|
||||
|
||||
if range.start > point || range.end < point {
|
||||
return None;
|
||||
}
|
||||
|
||||
if snapshot.is_line_folded(row) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let button = editor.render_breakpoint(text_anchor, point, &bp.kind, cx);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button,
|
||||
point,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
scroll_pixel_position,
|
||||
gutter_hitbox,
|
||||
rows_with_hunk_bounds,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
Some(button)
|
||||
})
|
||||
.collect_vec()
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
@@ -2042,6 +2116,7 @@ impl EditorElement {
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
snapshot: &EditorSnapshot,
|
||||
breakpoints: &mut HashMap<DisplayRow, (text::Anchor, Breakpoint)>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Vec<AnyElement> {
|
||||
@@ -2101,11 +2176,13 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let display_row = multibuffer_point.to_display_point(snapshot).row();
|
||||
let button = editor.render_run_indicator(
|
||||
&self.style,
|
||||
Some(display_row) == active_task_indicator_row,
|
||||
display_row,
|
||||
breakpoints.remove(&display_row),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -2135,6 +2212,7 @@ impl EditorElement {
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_hitbox: &Hitbox,
|
||||
rows_with_hunk_bounds: &HashMap<DisplayRow, Bounds<Pixels>>,
|
||||
breakpoint_points: &mut HashMap<DisplayRow, (text::Anchor, Breakpoint)>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
@@ -2149,11 +2227,16 @@ impl EditorElement {
|
||||
{
|
||||
active = deployed_from_indicator.map_or(true, |indicator_row| indicator_row == row);
|
||||
};
|
||||
button = editor.render_code_actions_indicator(&self.style, row, active, cx);
|
||||
|
||||
let breakpoint = breakpoint_points.get(&row);
|
||||
button = editor.render_code_actions_indicator(&self.style, row, active, breakpoint, cx);
|
||||
});
|
||||
|
||||
let button = button?;
|
||||
breakpoint_points.remove(&row);
|
||||
|
||||
let button = prepaint_gutter_button(
|
||||
button?,
|
||||
button,
|
||||
row,
|
||||
line_height,
|
||||
gutter_dimensions,
|
||||
@@ -2234,6 +2317,7 @@ impl EditorElement {
|
||||
scroll_position: gpui::Point<f32>,
|
||||
rows: Range<DisplayRow>,
|
||||
buffer_rows: &[RowInfo],
|
||||
active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
@@ -2289,7 +2373,18 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
|
||||
let color = cx.theme().colors().editor_line_number;
|
||||
let color = active_rows
|
||||
.get(&display_row)
|
||||
.and_then(|spec| {
|
||||
if spec.breakpoint {
|
||||
Some(cx.theme().colors().debugger_accent)
|
||||
} else if spec.selection {
|
||||
Some(cx.theme().colors().editor_active_line_number)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| cx.theme().colors().editor_line_number);
|
||||
let shaped_line = self
|
||||
.shape_line_number(SharedString::from(&line_number), color, window)
|
||||
.log_err()?;
|
||||
@@ -2320,7 +2415,6 @@ impl EditorElement {
|
||||
let line_number = LineNumberLayout {
|
||||
shaped_line,
|
||||
hitbox,
|
||||
display_row,
|
||||
};
|
||||
Some((multi_buffer_row, line_number))
|
||||
})
|
||||
@@ -2332,7 +2426,7 @@ impl EditorElement {
|
||||
&self,
|
||||
rows: Range<DisplayRow>,
|
||||
row_infos: &[RowInfo],
|
||||
active_rows: &BTreeMap<DisplayRow, bool>,
|
||||
active_rows: &BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
snapshot: &EditorSnapshot,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -4070,14 +4164,14 @@ impl EditorElement {
|
||||
.peek()
|
||||
.map_or(false, |(active_row, has_selection)| {
|
||||
active_row.0 == end_row + 1
|
||||
&& *has_selection == contains_non_empty_selection
|
||||
&& has_selection.selection == contains_non_empty_selection.selection
|
||||
})
|
||||
{
|
||||
active_rows.next().unwrap();
|
||||
end_row += 1;
|
||||
}
|
||||
|
||||
if !contains_non_empty_selection {
|
||||
if !contains_non_empty_selection.selection {
|
||||
let highlight_h_range =
|
||||
match layout.position_map.snapshot.current_line_highlight {
|
||||
CurrentLineHighlight::Gutter => Some(Range {
|
||||
@@ -4292,32 +4386,31 @@ impl EditorElement {
|
||||
for LineNumberLayout {
|
||||
shaped_line,
|
||||
hitbox,
|
||||
display_row,
|
||||
} in layout.line_numbers.values()
|
||||
{
|
||||
let Some(hitbox) = hitbox else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_active = layout.active_rows.contains_key(&display_row);
|
||||
let Some(()) = (if !is_singleton && hitbox.is_hovered(window) {
|
||||
let color = cx.theme().colors().editor_hover_line_number;
|
||||
|
||||
let color = if is_active {
|
||||
cx.theme().colors().editor_active_line_number
|
||||
} else if !is_singleton && hitbox.is_hovered(window) {
|
||||
cx.theme().colors().editor_hover_line_number
|
||||
let Some(line) = self
|
||||
.shape_line_number(shaped_line.text.clone(), color, window)
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
line.paint(hitbox.origin, line_height, window, cx).log_err()
|
||||
} else {
|
||||
cx.theme().colors().editor_line_number
|
||||
shaped_line
|
||||
.paint(hitbox.origin, line_height, window, cx)
|
||||
.log_err()
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(line) = self
|
||||
.shape_line_number(shaped_line.text.clone(), color, window)
|
||||
.log_err()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(()) = line.paint(hitbox.origin, line_height, window, cx).log_err() else {
|
||||
continue;
|
||||
};
|
||||
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
|
||||
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
|
||||
if is_singleton {
|
||||
@@ -4342,7 +4435,7 @@ impl EditorElement {
|
||||
&layout.position_map.snapshot,
|
||||
line_height,
|
||||
layout.gutter_hitbox.bounds,
|
||||
hunk,
|
||||
&hunk,
|
||||
);
|
||||
Some((
|
||||
hunk_bounds,
|
||||
@@ -4487,6 +4580,9 @@ impl EditorElement {
|
||||
}
|
||||
});
|
||||
|
||||
for breakpoint in layout.breakpoints.iter_mut() {
|
||||
breakpoint.paint(window, cx);
|
||||
}
|
||||
for test_indicator in layout.test_indicators.iter_mut() {
|
||||
test_indicator.paint(window, cx);
|
||||
}
|
||||
@@ -5658,6 +5754,7 @@ fn prepaint_gutter_button(
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let mut button = button.into_any_element();
|
||||
|
||||
let available_space = size(
|
||||
AvailableSpace::MinContent,
|
||||
AvailableSpace::Definite(line_height),
|
||||
@@ -6765,17 +6862,23 @@ impl Element for EditorElement {
|
||||
(selections, selected_buffer_ids)
|
||||
});
|
||||
|
||||
let (selections, active_rows, newest_selection_head) = self.layout_selections(
|
||||
start_anchor,
|
||||
end_anchor,
|
||||
&local_selections,
|
||||
&snapshot,
|
||||
start_row,
|
||||
end_row,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
let (selections, mut active_rows, newest_selection_head) = self
|
||||
.layout_selections(
|
||||
start_anchor,
|
||||
end_anchor,
|
||||
&local_selections,
|
||||
&snapshot,
|
||||
start_row,
|
||||
end_row,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let mut breakpoint_rows = self.editor.update(cx, |editor, cx| {
|
||||
editor.active_breakpoints(start_row..end_row, window, cx)
|
||||
});
|
||||
for display_row in breakpoint_rows.keys() {
|
||||
active_rows.entry(*display_row).or_default().breakpoint = true;
|
||||
}
|
||||
let line_numbers = self.layout_line_numbers(
|
||||
Some(&gutter_hitbox),
|
||||
gutter_dimensions,
|
||||
@@ -6783,12 +6886,32 @@ impl Element for EditorElement {
|
||||
scroll_position,
|
||||
start_row..end_row,
|
||||
&row_infos,
|
||||
&active_rows,
|
||||
newest_selection_head,
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
// We add the gutter breakpoint indicator to breakpoint_rows after painting
|
||||
// line numbers so we don't paint a line number debug accent color if a user
|
||||
// has their mouse over that line when a breakpoint isn't there
|
||||
let gutter_breakpoint_indicator =
|
||||
self.editor.read(cx).gutter_breakpoint_indicator;
|
||||
if let Some(gutter_breakpoint_point) = gutter_breakpoint_indicator {
|
||||
breakpoint_rows
|
||||
.entry(gutter_breakpoint_point.row())
|
||||
.or_insert_with(|| {
|
||||
let position = snapshot
|
||||
.display_point_to_breakpoint_anchor(gutter_breakpoint_point);
|
||||
let breakpoint = Breakpoint {
|
||||
kind: BreakpointKind::Standard,
|
||||
};
|
||||
|
||||
(position.text_anchor, breakpoint)
|
||||
});
|
||||
}
|
||||
|
||||
let mut crease_toggles =
|
||||
window.with_element_namespace("crease_toggles", |window| {
|
||||
self.layout_crease_toggles(
|
||||
@@ -7233,6 +7356,7 @@ impl Element for EditorElement {
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&mut breakpoint_rows,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -7262,6 +7386,7 @@ impl Element for EditorElement {
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&snapshot,
|
||||
&mut breakpoint_rows,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -7269,6 +7394,19 @@ impl Element for EditorElement {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let breakpoints = self.layout_breakpoints(
|
||||
line_height,
|
||||
start_row..end_row,
|
||||
scroll_pixel_position,
|
||||
&gutter_dimensions,
|
||||
&gutter_hitbox,
|
||||
&rows_with_hunk_bounds,
|
||||
&snapshot,
|
||||
breakpoint_rows,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.layout_signature_help(
|
||||
&hitbox,
|
||||
content_origin,
|
||||
@@ -7414,6 +7552,7 @@ impl Element for EditorElement {
|
||||
diff_hunk_controls,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
breakpoints,
|
||||
code_actions_indicator,
|
||||
crease_toggles,
|
||||
crease_trailers,
|
||||
@@ -7576,7 +7715,7 @@ pub struct EditorLayout {
|
||||
wrap_guides: SmallVec<[(Pixels, bool); 2]>,
|
||||
indent_guides: Option<Vec<IndentGuideLayout>>,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
active_rows: BTreeMap<DisplayRow, bool>,
|
||||
active_rows: BTreeMap<DisplayRow, LineHighlightSpec>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
|
||||
line_elements: SmallVec<[AnyElement; 1]>,
|
||||
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
|
||||
@@ -7593,6 +7732,7 @@ pub struct EditorLayout {
|
||||
selections: Vec<(PlayerColor, Vec<SelectionLayout>)>,
|
||||
code_actions_indicator: Option<AnyElement>,
|
||||
test_indicators: Vec<AnyElement>,
|
||||
breakpoints: Vec<AnyElement>,
|
||||
crease_toggles: Vec<Option<AnyElement>>,
|
||||
diff_hunk_controls: Vec<AnyElement>,
|
||||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||||
@@ -7612,7 +7752,6 @@ impl EditorLayout {
|
||||
struct LineNumberLayout {
|
||||
shaped_line: ShapedLine,
|
||||
hitbox: Option<Hitbox>,
|
||||
display_row: DisplayRow,
|
||||
}
|
||||
|
||||
struct ColoredRange<T> {
|
||||
@@ -8262,8 +8401,10 @@ mod tests {
|
||||
..Default::default()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
&BTreeMap::default(),
|
||||
Some(DisplayPoint::new(DisplayRow(0), 0)),
|
||||
&snapshot,
|
||||
&HashMap::default(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use fs::Fs;
|
||||
use gpui::{App, Global, ReadGlobal, SharedString, Task};
|
||||
use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage};
|
||||
use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
@@ -284,7 +284,7 @@ pub trait ExtensionLanguageServerProxy: Send + Sync + 'static {
|
||||
fn update_language_server_status(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
status: BinaryStatus,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -317,7 +317,7 @@ impl ExtensionLanguageServerProxy for ExtensionHostProxy {
|
||||
fn update_language_server_status(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
status: BinaryStatus,
|
||||
) {
|
||||
let Some(proxy) = self.language_server_proxy.read().clone() else {
|
||||
return;
|
||||
|
||||
@@ -8,9 +8,9 @@ use collections::BTreeMap;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs, RealFs};
|
||||
use futures::{io::BufReader, AsyncReadExt, StreamExt};
|
||||
use gpui::{AppContext as _, SemanticVersion, TestAppContext};
|
||||
use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext};
|
||||
use http_client::{FakeHttpClient, Response};
|
||||
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};
|
||||
use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
|
||||
use lsp::LanguageServerName;
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
@@ -660,18 +660,9 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
status_updates.next().await.unwrap(),
|
||||
],
|
||||
[
|
||||
(
|
||||
LanguageServerName("gleam".into()),
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
),
|
||||
(
|
||||
LanguageServerName("gleam".into()),
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
),
|
||||
(
|
||||
LanguageServerName("gleam".into()),
|
||||
LanguageServerBinaryStatus::None
|
||||
)
|
||||
(SharedString::new("gleam"), BinaryStatus::CheckingForUpdate),
|
||||
(SharedString::new("gleam"), BinaryStatus::Downloading),
|
||||
(SharedString::new("gleam"), BinaryStatus::None)
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::wasm_host::wit::since_v0_0_4;
|
||||
use crate::wasm_host::WasmState;
|
||||
use anyhow::Result;
|
||||
use extension::{ExtensionLanguageServerProxy, WorktreeDelegate};
|
||||
use language::LanguageServerBinaryStatus;
|
||||
use language::BinaryStatus;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
@@ -132,17 +132,11 @@ impl ExtensionImports for WasmState {
|
||||
status: LanguageServerInstallationStatus,
|
||||
) -> wasmtime::Result<()> {
|
||||
let status = match status {
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
}
|
||||
LanguageServerInstallationStatus::Downloading => {
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
}
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
|
||||
LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
|
||||
LanguageServerInstallationStatus::Cached
|
||||
| LanguageServerInstallationStatus::Downloaded => LanguageServerBinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => {
|
||||
LanguageServerBinaryStatus::Failed { error }
|
||||
}
|
||||
| LanguageServerInstallationStatus::Downloaded => BinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
|
||||
};
|
||||
|
||||
self.host
|
||||
|
||||
@@ -8,7 +8,7 @@ use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDel
|
||||
use futures::{io::BufReader, FutureExt as _};
|
||||
use futures::{lock::Mutex, AsyncReadExt};
|
||||
use language::LanguageName;
|
||||
use language::{language_settings::AllLanguageSettings, LanguageServerBinaryStatus};
|
||||
use language::{language_settings::AllLanguageSettings, BinaryStatus};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::{
|
||||
@@ -474,16 +474,10 @@ impl ExtensionImports for WasmState {
|
||||
status: LanguageServerInstallationStatus,
|
||||
) -> wasmtime::Result<()> {
|
||||
let status = match status {
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
}
|
||||
LanguageServerInstallationStatus::Downloading => {
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
}
|
||||
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => {
|
||||
LanguageServerBinaryStatus::Failed { error }
|
||||
}
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
|
||||
LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
|
||||
LanguageServerInstallationStatus::None => BinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
|
||||
};
|
||||
|
||||
self.host
|
||||
|
||||
@@ -13,7 +13,7 @@ use extension::{
|
||||
};
|
||||
use futures::{io::BufReader, FutureExt as _};
|
||||
use futures::{lock::Mutex, AsyncReadExt};
|
||||
use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus};
|
||||
use language::{language_settings::AllLanguageSettings, BinaryStatus, LanguageName};
|
||||
use project::project_settings::ProjectSettings;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::{
|
||||
@@ -690,16 +690,10 @@ impl ExtensionImports for WasmState {
|
||||
status: LanguageServerInstallationStatus,
|
||||
) -> wasmtime::Result<()> {
|
||||
let status = match status {
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate
|
||||
}
|
||||
LanguageServerInstallationStatus::Downloading => {
|
||||
LanguageServerBinaryStatus::Downloading
|
||||
}
|
||||
LanguageServerInstallationStatus::None => LanguageServerBinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => {
|
||||
LanguageServerBinaryStatus::Failed { error }
|
||||
}
|
||||
LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate,
|
||||
LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading,
|
||||
LanguageServerInstallationStatus::None => BinaryStatus::None,
|
||||
LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error },
|
||||
};
|
||||
|
||||
self.host
|
||||
|
||||
@@ -120,6 +120,11 @@ impl FeatureFlag for AutoCommand {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Debugger {}
|
||||
impl FeatureFlag for Debugger {
|
||||
const NAME: &'static str = "debugger";
|
||||
}
|
||||
|
||||
pub trait FeatureFlagViewExt<V: 'static> {
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription
|
||||
where
|
||||
|
||||
@@ -73,8 +73,8 @@ pub use buffer::Operation;
|
||||
pub use buffer::*;
|
||||
pub use diagnostic_set::{DiagnosticEntry, DiagnosticGroup};
|
||||
pub use language_registry::{
|
||||
AvailableLanguage, LanguageNotFound, LanguageQueries, LanguageRegistry,
|
||||
LanguageServerBinaryStatus, QUERY_FILENAME_PREFIXES,
|
||||
AvailableLanguage, BinaryStatus, LanguageNotFound, LanguageQueries, LanguageRegistry,
|
||||
QUERY_FILENAME_PREFIXES,
|
||||
};
|
||||
pub use lsp::{LanguageServerId, LanguageServerName};
|
||||
pub use outline::*;
|
||||
@@ -304,7 +304,7 @@ pub trait LspAdapterDelegate: Send + Sync {
|
||||
fn worktree_id(&self) -> WorktreeId;
|
||||
fn worktree_root_path(&self) -> &Path;
|
||||
fn exists(&self, path: &Path, is_dir: Option<bool>) -> bool;
|
||||
fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
|
||||
fn update_status(&self, language: LanguageServerName, status: BinaryStatus);
|
||||
async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;
|
||||
|
||||
async fn npm_package_installed_version(
|
||||
@@ -382,7 +382,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
} else {
|
||||
delegate.update_status(
|
||||
self.name(),
|
||||
LanguageServerBinaryStatus::Failed {
|
||||
BinaryStatus::Failed {
|
||||
error: format!("{error:?}"),
|
||||
},
|
||||
);
|
||||
@@ -569,7 +569,7 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
|
||||
|
||||
let name = adapter.name();
|
||||
log::info!("fetching latest version of language server {:?}", name.0);
|
||||
delegate.update_status(name.clone(), LanguageServerBinaryStatus::CheckingForUpdate);
|
||||
delegate.update_status(name.clone(), BinaryStatus::CheckingForUpdate);
|
||||
|
||||
let latest_version = adapter
|
||||
.fetch_latest_server_version(delegate.as_ref())
|
||||
@@ -580,16 +580,16 @@ async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>
|
||||
.await
|
||||
{
|
||||
log::info!("language server {:?} is already installed", name.0);
|
||||
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
|
||||
delegate.update_status(name.clone(), BinaryStatus::None);
|
||||
Ok(binary)
|
||||
} else {
|
||||
log::info!("downloading language server {:?}", name.0);
|
||||
delegate.update_status(adapter.name(), LanguageServerBinaryStatus::Downloading);
|
||||
delegate.update_status(adapter.name(), BinaryStatus::Downloading);
|
||||
let binary = adapter
|
||||
.fetch_server_binary(latest_version, container_dir, delegate.as_ref())
|
||||
.await;
|
||||
|
||||
delegate.update_status(name.clone(), LanguageServerBinaryStatus::None);
|
||||
delegate.update_status(name.clone(), BinaryStatus::None);
|
||||
binary
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,8 @@ pub struct LanguageRegistry {
|
||||
state: RwLock<LanguageRegistryState>,
|
||||
language_server_download_dir: Option<Arc<Path>>,
|
||||
executor: BackgroundExecutor,
|
||||
lsp_binary_status_tx: LspBinaryStatusSender,
|
||||
lsp_binary_status_tx: BinaryStatusSender,
|
||||
dap_binary_status_tx: BinaryStatusSender,
|
||||
}
|
||||
|
||||
struct LanguageRegistryState {
|
||||
@@ -130,7 +131,7 @@ pub struct FakeLanguageServerEntry {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LanguageServerBinaryStatus {
|
||||
pub enum BinaryStatus {
|
||||
None,
|
||||
CheckingForUpdate,
|
||||
Downloading,
|
||||
@@ -213,8 +214,8 @@ pub struct LanguageQueries {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct LspBinaryStatusSender {
|
||||
txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerBinaryStatus)>>>>,
|
||||
struct BinaryStatusSender {
|
||||
txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(SharedString, BinaryStatus)>>>>,
|
||||
}
|
||||
|
||||
pub struct LoadedLanguage {
|
||||
@@ -247,6 +248,7 @@ impl LanguageRegistry {
|
||||
}),
|
||||
language_server_download_dir: None,
|
||||
lsp_binary_status_tx: Default::default(),
|
||||
dap_binary_status_tx: Default::default(),
|
||||
executor,
|
||||
};
|
||||
this.add(PLAIN_TEXT.clone());
|
||||
@@ -914,12 +916,12 @@ impl LanguageRegistry {
|
||||
self.state.read().all_lsp_adapters.get(name).cloned()
|
||||
}
|
||||
|
||||
pub fn update_lsp_status(
|
||||
&self,
|
||||
server_name: LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
) {
|
||||
self.lsp_binary_status_tx.send(server_name, status);
|
||||
pub fn update_lsp_status(&self, server_name: LanguageServerName, status: BinaryStatus) {
|
||||
self.lsp_binary_status_tx.send(server_name.0, status);
|
||||
}
|
||||
|
||||
pub fn update_dap_status(&self, server_name: LanguageServerName, status: BinaryStatus) {
|
||||
self.dap_binary_status_tx.send(server_name.0, status);
|
||||
}
|
||||
|
||||
pub fn next_language_server_id(&self) -> LanguageServerId {
|
||||
@@ -974,10 +976,16 @@ impl LanguageRegistry {
|
||||
|
||||
pub fn language_server_binary_statuses(
|
||||
&self,
|
||||
) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
|
||||
) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
|
||||
self.lsp_binary_status_tx.subscribe()
|
||||
}
|
||||
|
||||
pub fn dap_server_binary_statuses(
|
||||
&self,
|
||||
) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
|
||||
self.dap_binary_status_tx.subscribe()
|
||||
}
|
||||
|
||||
pub async fn delete_server_container(&self, name: LanguageServerName) {
|
||||
log::info!("deleting server container");
|
||||
let Some(dir) = self.language_server_download_dir(&name) else {
|
||||
@@ -1088,16 +1096,14 @@ impl LanguageRegistryState {
|
||||
}
|
||||
}
|
||||
|
||||
impl LspBinaryStatusSender {
|
||||
fn subscribe(
|
||||
&self,
|
||||
) -> mpsc::UnboundedReceiver<(LanguageServerName, LanguageServerBinaryStatus)> {
|
||||
impl BinaryStatusSender {
|
||||
fn subscribe(&self) -> mpsc::UnboundedReceiver<(SharedString, BinaryStatus)> {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
self.txs.lock().push(tx);
|
||||
rx
|
||||
}
|
||||
|
||||
fn send(&self, name: LanguageServerName, status: LanguageServerBinaryStatus) {
|
||||
fn send(&self, name: SharedString, status: BinaryStatus) {
|
||||
let mut txs = self.txs.lock();
|
||||
txs.retain(|tx| tx.unbounded_send((name.clone(), status.clone())).is_ok());
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ use fs::Fs;
|
||||
use futures::{Future, FutureExt};
|
||||
use gpui::AsyncApp;
|
||||
use language::{
|
||||
CodeLabel, HighlightId, Language, LanguageName, LanguageServerBinaryStatus,
|
||||
LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
|
||||
BinaryStatus, CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore,
|
||||
LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};
|
||||
use serde::Serialize;
|
||||
@@ -80,7 +80,7 @@ impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy {
|
||||
fn update_language_server_status(
|
||||
&self,
|
||||
language_server_id: LanguageServerName,
|
||||
status: LanguageServerBinaryStatus,
|
||||
status: BinaryStatus,
|
||||
) {
|
||||
self.language_registry
|
||||
.update_lsp_status(language_server_id, status);
|
||||
|
||||
@@ -85,6 +85,7 @@ impl JsonLspAdapter {
|
||||
cx,
|
||||
);
|
||||
let tasks_schema = task::TaskTemplates::generate_json_schema();
|
||||
let debug_schema = task::DebugTaskFile::generate_json_schema();
|
||||
let snippets_schema = snippet_provider::format::VSSnippetsFile::generate_json_schema();
|
||||
let tsconfig_schema = serde_json::Value::from_str(TSCONFIG_SCHEMA).unwrap();
|
||||
let package_json_schema = serde_json::Value::from_str(PACKAGE_JSON_SCHEMA).unwrap();
|
||||
@@ -136,7 +137,15 @@ impl JsonLspAdapter {
|
||||
)
|
||||
],
|
||||
"schema": snippets_schema,
|
||||
}
|
||||
},
|
||||
{
|
||||
"fileMatch": [
|
||||
schema_file_match(paths::debug_tasks_file()),
|
||||
paths::local_debug_file_relative_path()
|
||||
],
|
||||
"schema": debug_schema,
|
||||
|
||||
},
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4822,6 +4822,16 @@ impl MultiBufferSnapshot {
|
||||
self.anchor_at(position, Bias::Right)
|
||||
}
|
||||
|
||||
pub fn breakpoint_anchor<T: ToOffset>(&self, position: T) -> Anchor {
|
||||
let bias = if position.to_offset(self) == 0usize {
|
||||
Bias::Right
|
||||
} else {
|
||||
Bias::Left
|
||||
};
|
||||
|
||||
self.anchor_at(position, bias)
|
||||
}
|
||||
|
||||
pub fn anchor_at<T: ToOffset>(&self, position: T, mut bias: Bias) -> Anchor {
|
||||
let offset = position.to_offset(self);
|
||||
|
||||
|
||||
@@ -169,6 +169,12 @@ pub fn tasks_file() -> &'static PathBuf {
|
||||
TASKS_FILE.get_or_init(|| config_dir().join("tasks.json"))
|
||||
}
|
||||
|
||||
/// Returns the path to the `debug.json` file.
|
||||
pub fn debug_tasks_file() -> &'static PathBuf {
|
||||
static DEBUG_TASKS_FILE: OnceLock<PathBuf> = OnceLock::new();
|
||||
DEBUG_TASKS_FILE.get_or_init(|| config_dir().join("debug.json"))
|
||||
}
|
||||
|
||||
/// Returns the path to the extensions directory.
|
||||
///
|
||||
/// This is where installed extensions are stored.
|
||||
@@ -284,6 +290,14 @@ pub fn languages_dir() -> &'static PathBuf {
|
||||
LANGUAGES_DIR.get_or_init(|| support_dir().join("languages"))
|
||||
}
|
||||
|
||||
/// Returns the path to the debug adapters directory
|
||||
///
|
||||
/// This is where debug adapters are downloaded to for DAPs that are built-in to Zed.
|
||||
pub fn debug_adapters_dir() -> &'static PathBuf {
|
||||
static DEBUG_ADAPTERS_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
DEBUG_ADAPTERS_DIR.get_or_init(|| support_dir().join("debug_adapters"))
|
||||
}
|
||||
|
||||
/// Returns the path to the Copilot directory.
|
||||
pub fn copilot_dir() -> &'static PathBuf {
|
||||
static COPILOT_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
@@ -328,5 +342,15 @@ pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
|
||||
Path::new(".vscode/tasks.json")
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `launch.json` file within a project.
|
||||
pub fn local_debug_file_relative_path() -> &'static Path {
|
||||
Path::new(".zed/debug.json")
|
||||
}
|
||||
|
||||
/// Returns the relative path to a `.vscode/launch.json` file within a project.
|
||||
pub fn local_vscode_launch_file_relative_path() -> &'static Path {
|
||||
Path::new(".vscode/launch.json")
|
||||
}
|
||||
|
||||
/// A default editorconfig file name to use when resolving project settings.
|
||||
pub const EDITORCONFIG_NAME: &str = ".editorconfig";
|
||||
|
||||
@@ -22,16 +22,20 @@ test-support = [
|
||||
"prettier/test-support",
|
||||
"worktree/test-support",
|
||||
"gpui/test-support",
|
||||
"dap/test-support",
|
||||
"dap_adapters/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
aho-corasick.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
dap.workspace = true
|
||||
dap_adapters.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
@@ -40,6 +44,7 @@ globset.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
indexmap.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
@@ -79,17 +84,19 @@ fancy-regex.workspace = true
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
dap = { workspace = true, features = ["test-support"] }
|
||||
dap_adapters = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
git2.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
prettier = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
worktree = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -330,6 +330,10 @@ enum OpenBuffer {
|
||||
|
||||
pub enum BufferStoreEvent {
|
||||
BufferAdded(Entity<Buffer>),
|
||||
BufferOpened {
|
||||
buffer: Entity<Buffer>,
|
||||
project_path: ProjectPath,
|
||||
},
|
||||
BufferDropped(BufferId),
|
||||
BufferChangedFilePath {
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -1280,6 +1284,13 @@ impl BufferStore {
|
||||
}
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<&LocalBufferStore> {
|
||||
match &self.state {
|
||||
BufferStoreState::Local(state) => Some(state),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> {
|
||||
match &mut self.state {
|
||||
BufferStoreState::Local(state) => Some(state),
|
||||
@@ -1307,6 +1318,11 @@ impl BufferStore {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<Buffer>>> {
|
||||
if let Some(buffer) = self.get_by_path(&project_path, cx) {
|
||||
cx.emit(BufferStoreEvent::BufferOpened {
|
||||
buffer: buffer.clone(),
|
||||
project_path,
|
||||
});
|
||||
|
||||
return Task::ready(Ok(buffer));
|
||||
}
|
||||
|
||||
@@ -1330,12 +1346,18 @@ impl BufferStore {
|
||||
.insert(
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let load_result = load_buffer.await;
|
||||
this.update(&mut cx, |this, _cx| {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
// Record the fact that the buffer is no longer loading.
|
||||
this.loading_buffers.remove(&project_path);
|
||||
})
|
||||
.ok();
|
||||
load_result.map_err(Arc::new)
|
||||
|
||||
let buffer = load_result.map_err(Arc::new)?;
|
||||
cx.emit(BufferStoreEvent::BufferOpened {
|
||||
buffer: buffer.clone(),
|
||||
project_path,
|
||||
});
|
||||
|
||||
Ok(buffer)
|
||||
})?
|
||||
})
|
||||
.shared(),
|
||||
)
|
||||
@@ -1771,6 +1793,11 @@ impl BufferStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn buffer_id_for_project_path(&self, project_path: &ProjectPath) -> Option<&BufferId> {
|
||||
self.as_local()
|
||||
.and_then(|state| state.local_buffer_ids_by_path.get(project_path))
|
||||
}
|
||||
|
||||
pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option<Entity<Buffer>> {
|
||||
self.buffers().find_map(|buffer| {
|
||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
||||
|
||||
17
crates/project/src/debugger.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
//! Zed's debugger data layer is implemented in terms of 3 concepts:
|
||||
//! - DAP store - that knows about all of the available debug sessions.
|
||||
//! - Debug sessions - that bear responsibility of communicating with debug adapters and managing the state of each individual session.
|
||||
//! For the most part it is agnostic over the communication layer (it'll use RPC for peers and actual DAP requests for the host).
|
||||
//! - Breakpoint store - that knows about all breakpoints set for a project.
|
||||
//!
|
||||
//! There are few reasons for this divide:
|
||||
//! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them
|
||||
//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up.
|
||||
//! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain
|
||||
//! current set of breakpoints.
|
||||
//! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session.
|
||||
|
||||
pub mod breakpoint_store;
|
||||
pub mod dap_command;
|
||||
pub mod dap_store;
|
||||
pub mod session;
|
||||