Compare commits
782 Commits
acp-rewind
...
breakpoint
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4568078e7 | ||
|
|
d325d4bda4 | ||
|
|
e257403262 | ||
|
|
8b59056b1e | ||
|
|
2af93fe609 | ||
|
|
0797e2a97a | ||
|
|
6021f04a59 | ||
|
|
fa3cf3e1df | ||
|
|
62f2bc8a8d | ||
|
|
17c7ede60c | ||
|
|
f95c4df237 | ||
|
|
393fe5f71f | ||
|
|
f8eebc73f5 | ||
|
|
43bf537bba | ||
|
|
2dfe6cdffc | ||
|
|
bae466e4b5 | ||
|
|
0eee9a45e5 | ||
|
|
d14b02272d | ||
|
|
cb2fda0f3f | ||
|
|
0a59016961 | ||
|
|
12cead7bc9 | ||
|
|
f4bd652745 | ||
|
|
cba76fe29b | ||
|
|
9a7ecdc224 | ||
|
|
6d2f113528 | ||
|
|
cc5bcd80b1 | ||
|
|
63e2ca3335 | ||
|
|
f12b3c9147 | ||
|
|
3c65060606 | ||
|
|
d05b4905c5 | ||
|
|
66d6b794a1 | ||
|
|
37b29e11ee | ||
|
|
5b470a6870 | ||
|
|
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 {}
|
||||
24
crates/dap/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
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};
|
||||
|
||||
#[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
|
||||
303
crates/debugger_ui/src/attach_modal.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
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(|_| (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();
|
||||
}
|
||||
271
crates/debugger_ui/src/session.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
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 ThreadItem {
|
||||
fn _to_proto(&self) -> proto::DebuggerThreadItem {
|
||||
match self {
|
||||
ThreadItem::Console => proto::DebuggerThreadItem::Console,
|
||||
ThreadItem::LoadedSource => proto::DebuggerThreadItem::LoadedSource,
|
||||
ThreadItem::Modules => proto::DebuggerThreadItem::Modules,
|
||||
ThreadItem::Variables => proto::DebuggerThreadItem::Variables,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_proto(active_thread_item: proto::DebuggerThreadItem) -> Self {
|
||||
match active_thread_item {
|
||||
proto::DebuggerThreadItem::Console => ThreadItem::Console,
|
||||
proto::DebuggerThreadItem::LoadedSource => ThreadItem::LoadedSource,
|
||||
proto::DebuggerThreadItem::Modules => ThreadItem::Modules,
|
||||
proto::DebuggerThreadItem::Variables => ThreadItem::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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
681
crates/debugger_ui/src/session/running.rs
Normal file
@@ -0,0 +1,681 @@
|
||||
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(_)
|
||||
| StackFrameListEvent::StackFramesUpdated => 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 { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
508
crates/debugger_ui/src/session/running/console.rs
Normal file
@@ -0,0 +1,508 @@
|
||||
use super::{
|
||||
stack_frame_list::{StackFrameList, StackFrameListEvent},
|
||||
variable_list::VariableList,
|
||||
};
|
||||
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, collections::HashMap, 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(),
|
||||
StackFrameListEvent::StackFramesUpdated => {}
|
||||
}
|
||||
}
|
||||
|
||||
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.variable.dap.evaluate_name {
|
||||
variables.insert(evaluate_name.clone(), variable.variable.dap.value.clone());
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: evaluate_name.clone(),
|
||||
char_bag: evaluate_name.chars().collect(),
|
||||
});
|
||||
}
|
||||
|
||||
variables.insert(
|
||||
variable.variable.dap.name.clone(),
|
||||
variable.variable.dap.value.clone(),
|
||||
);
|
||||
|
||||
string_matches.push(StringMatchCandidate {
|
||||
id: 0,
|
||||
string: variable.variable.dap.name.clone(),
|
||||
char_bag: variable.variable.dap.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()
|
||||
})
|
||||
}
|
||||
}
|
||||
485
crates/debugger_ui/src/session/running/stack_frame_list.rs
Normal file
@@ -0,0 +1,485 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
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;
|
||||
|
||||
pub type StackFrameId = u64;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum StackFrameListEvent {
|
||||
SelectedStackFrameChanged(StackFrameId),
|
||||
StackFramesUpdated,
|
||||
}
|
||||
|
||||
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 {}
|
||||
1244
crates/debugger_ui/src/session/running/variable_list.rs
Normal file
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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::wasm_host::WasmState;
|
||||
use anyhow::Result;
|
||||
use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate};
|
||||
use language::BinaryStatus;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
@@ -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())?;
|
||||
|
||||