1#![expect(clippy::result_large_err)]
2use crate::{
3 persistence::DebuggerPaneItem,
4 tests::{start_debug_session, start_debug_session_with},
5 *,
6};
7use dap::{
8 ErrorResponse, Message, RunInTerminalRequestArguments, SourceBreakpoint,
9 StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
10 adapters::DebugTaskDefinition,
11 client::SessionId,
12 requests::{
13 Continue, Disconnect, Launch, Next, RunInTerminal, SetBreakpoints, StackTrace,
14 StartDebugging, StepBack, StepIn, StepOut, Threads,
15 },
16};
17use editor::{
18 ActiveDebugLine, Editor, EditorMode, MultiBuffer,
19 actions::{self},
20};
21use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
22use project::{
23 FakeFs, Project,
24 debugger::session::{ThreadId, ThreadStatus},
25};
26use serde_json::json;
27use std::{
28 path::Path,
29 sync::{
30 Arc,
31 atomic::{AtomicBool, AtomicUsize, Ordering},
32 },
33};
34use terminal_view::terminal_panel::TerminalPanel;
35use tests::{active_debug_session_panel, init_test, init_test_workspace};
36use util::{path, rel_path::rel_path};
37use workspace::item::SaveOptions;
38use workspace::pane_group::SplitDirection;
39use workspace::{Item, dock::Panel, move_active_item};
40
41#[gpui::test]
42async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut TestAppContext) {
43 init_test(cx);
44
45 let fs = FakeFs::new(executor.clone());
46
47 fs.insert_tree(
48 path!("/project"),
49 json!({
50 "main.rs": "First line\nSecond line\nThird line\nFourth line",
51 }),
52 )
53 .await;
54
55 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
56 let workspace = init_test_workspace(&project, cx).await;
57 let cx = &mut VisualTestContext::from_window(*workspace, cx);
58
59 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
60 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
61
62 client.on_request::<Threads, _>(move |_, _| {
63 Ok(dap::ThreadsResponse {
64 threads: vec![dap::Thread {
65 id: 1,
66 name: "Thread 1".into(),
67 }],
68 })
69 });
70
71 client.on_request::<StackTrace, _>(move |_, _| {
72 Ok(dap::StackTraceResponse {
73 stack_frames: Vec::default(),
74 total_frames: None,
75 })
76 });
77
78 cx.run_until_parked();
79
80 // assert we have a debug panel item before the session has stopped
81 workspace
82 .update(cx, |workspace, _window, cx| {
83 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
84 let active_session =
85 debug_panel.update(cx, |debug_panel, _| debug_panel.active_session().unwrap());
86
87 let running_state = active_session.update(cx, |active_session, _| {
88 active_session.running_state().clone()
89 });
90
91 debug_panel.update(cx, |this, cx| {
92 assert!(this.active_session().is_some());
93 assert!(running_state.read(cx).selected_thread_id().is_none());
94 });
95 })
96 .unwrap();
97
98 client
99 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
100 reason: dap::StoppedEventReason::Pause,
101 description: None,
102 thread_id: Some(1),
103 preserve_focus_hint: None,
104 text: None,
105 all_threads_stopped: None,
106 hit_breakpoint_ids: None,
107 }))
108 .await;
109
110 cx.run_until_parked();
111
112 workspace
113 .update(cx, |workspace, _window, cx| {
114 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
115 let active_session = debug_panel
116 .update(cx, |this, _| this.active_session())
117 .unwrap();
118
119 let running_state = active_session.update(cx, |active_session, _| {
120 active_session.running_state().clone()
121 });
122
123 assert_eq!(client.id(), running_state.read(cx).session_id());
124 assert_eq!(
125 ThreadId(1),
126 running_state.read(cx).selected_thread_id().unwrap()
127 );
128 })
129 .unwrap();
130
131 let shutdown_session = project.update(cx, |project, cx| {
132 project.dap_store().update(cx, |dap_store, cx| {
133 dap_store.shutdown_session(session.read(cx).session_id(), cx)
134 })
135 });
136
137 shutdown_session.await.unwrap();
138
139 // assert we still have a debug panel item after the client shutdown
140 workspace
141 .update(cx, |workspace, _window, cx| {
142 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
143
144 let active_session = debug_panel
145 .update(cx, |this, _| this.active_session())
146 .unwrap();
147
148 let running_state = active_session.update(cx, |active_session, _| {
149 active_session.running_state().clone()
150 });
151
152 debug_panel.update(cx, |this, cx| {
153 assert!(this.active_session().is_some());
154 assert_eq!(
155 ThreadId(1),
156 running_state.read(cx).selected_thread_id().unwrap()
157 );
158 });
159 })
160 .unwrap();
161}
162
163#[gpui::test]
164async fn test_we_can_only_have_one_panel_per_debug_session(
165 executor: BackgroundExecutor,
166 cx: &mut TestAppContext,
167) {
168 init_test(cx);
169
170 let fs = FakeFs::new(executor.clone());
171
172 fs.insert_tree(
173 path!("/project"),
174 json!({
175 "main.rs": "First line\nSecond line\nThird line\nFourth line",
176 }),
177 )
178 .await;
179
180 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
181 let workspace = init_test_workspace(&project, cx).await;
182 let cx = &mut VisualTestContext::from_window(*workspace, cx);
183
184 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
185 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
186
187 client.on_request::<Threads, _>(move |_, _| {
188 Ok(dap::ThreadsResponse {
189 threads: vec![dap::Thread {
190 id: 1,
191 name: "Thread 1".into(),
192 }],
193 })
194 });
195
196 client.on_request::<StackTrace, _>(move |_, _| {
197 Ok(dap::StackTraceResponse {
198 stack_frames: Vec::default(),
199 total_frames: None,
200 })
201 });
202
203 cx.run_until_parked();
204
205 // assert we have a debug panel item before the session has stopped
206 workspace
207 .update(cx, |workspace, _window, cx| {
208 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
209
210 debug_panel.update(cx, |this, _| {
211 assert!(this.active_session().is_some());
212 });
213 })
214 .unwrap();
215
216 client
217 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
218 reason: dap::StoppedEventReason::Pause,
219 description: None,
220 thread_id: Some(1),
221 preserve_focus_hint: None,
222 text: None,
223 all_threads_stopped: None,
224 hit_breakpoint_ids: None,
225 }))
226 .await;
227
228 cx.run_until_parked();
229
230 // assert we added a debug panel item
231 workspace
232 .update(cx, |workspace, _window, cx| {
233 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
234 let active_session = debug_panel
235 .update(cx, |this, _| this.active_session())
236 .unwrap();
237
238 let running_state = active_session.update(cx, |active_session, _| {
239 active_session.running_state().clone()
240 });
241
242 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
243 assert_eq!(
244 ThreadId(1),
245 running_state.read(cx).selected_thread_id().unwrap()
246 );
247 })
248 .unwrap();
249
250 client
251 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
252 reason: dap::StoppedEventReason::Pause,
253 description: None,
254 thread_id: Some(2),
255 preserve_focus_hint: None,
256 text: None,
257 all_threads_stopped: None,
258 hit_breakpoint_ids: None,
259 }))
260 .await;
261
262 cx.run_until_parked();
263
264 workspace
265 .update(cx, |workspace, _window, cx| {
266 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
267 let active_session = debug_panel
268 .update(cx, |this, _| this.active_session())
269 .unwrap();
270
271 let running_state = active_session.update(cx, |active_session, _| {
272 active_session.running_state().clone()
273 });
274
275 assert_eq!(client.id(), active_session.read(cx).session_id(cx));
276 assert_eq!(
277 ThreadId(1),
278 running_state.read(cx).selected_thread_id().unwrap()
279 );
280 })
281 .unwrap();
282
283 let shutdown_session = project.update(cx, |project, cx| {
284 project.dap_store().update(cx, |dap_store, cx| {
285 dap_store.shutdown_session(session.read(cx).session_id(), cx)
286 })
287 });
288
289 shutdown_session.await.unwrap();
290
291 // assert we still have a debug panel item after the client shutdown
292 workspace
293 .update(cx, |workspace, _window, cx| {
294 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
295 let active_session = debug_panel
296 .update(cx, |this, _| this.active_session())
297 .unwrap();
298
299 let running_state = active_session.update(cx, |active_session, _| {
300 active_session.running_state().clone()
301 });
302
303 debug_panel.update(cx, |this, cx| {
304 assert!(this.active_session().is_some());
305 assert_eq!(
306 ThreadId(1),
307 running_state.read(cx).selected_thread_id().unwrap()
308 );
309 });
310 })
311 .unwrap();
312}
313
314#[gpui::test]
315async fn test_handle_successful_run_in_terminal_reverse_request(
316 executor: BackgroundExecutor,
317 cx: &mut TestAppContext,
318) {
319 // needed because the debugger launches a terminal which starts a background PTY
320 cx.executor().allow_parking();
321 init_test(cx);
322
323 let send_response = Arc::new(AtomicBool::new(false));
324
325 let fs = FakeFs::new(executor.clone());
326
327 fs.insert_tree(
328 path!("/project"),
329 json!({
330 "main.rs": "First line\nSecond line\nThird line\nFourth line",
331 }),
332 )
333 .await;
334
335 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
336 let workspace = init_test_workspace(&project, cx).await;
337 let cx = &mut VisualTestContext::from_window(*workspace, cx);
338
339 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
340 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
341
342 client
343 .on_response::<RunInTerminal, _>({
344 let send_response = send_response.clone();
345 move |response| {
346 send_response.store(true, Ordering::SeqCst);
347
348 assert!(response.success);
349 assert!(response.body.is_some());
350 }
351 })
352 .await;
353
354 client
355 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
356 kind: None,
357 title: None,
358 cwd: std::env::temp_dir().to_string_lossy().into_owned(),
359 args: vec![],
360 env: None,
361 args_can_be_interpreted_by_shell: None,
362 })
363 .await;
364
365 cx.run_until_parked();
366
367 assert!(
368 send_response.load(std::sync::atomic::Ordering::SeqCst),
369 "Expected to receive response from reverse request"
370 );
371
372 workspace
373 .update(cx, |workspace, _window, cx| {
374 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
375 let session = debug_panel.read(cx).active_session().unwrap();
376 let running = session.read(cx).running_state();
377 assert_eq!(
378 running
379 .read(cx)
380 .pane_items_status(cx)
381 .get(&DebuggerPaneItem::Terminal),
382 Some(&true)
383 );
384 assert!(running.read(cx).debug_terminal.read(cx).terminal.is_some());
385 })
386 .unwrap();
387}
388
389#[gpui::test]
390async fn test_handle_start_debugging_request(
391 executor: BackgroundExecutor,
392 cx: &mut TestAppContext,
393) {
394 init_test(cx);
395
396 let fs = FakeFs::new(executor.clone());
397
398 fs.insert_tree(
399 path!("/project"),
400 json!({
401 "main.rs": "First line\nSecond line\nThird line\nFourth line",
402 }),
403 )
404 .await;
405
406 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
407 let workspace = init_test_workspace(&project, cx).await;
408 let cx = &mut VisualTestContext::from_window(*workspace, cx);
409
410 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
411 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
412
413 let fake_config = json!({"one": "two"});
414 let launched_with = Arc::new(parking_lot::Mutex::new(None));
415
416 let _subscription = project::debugger::test::intercept_debug_sessions(cx, {
417 let launched_with = launched_with.clone();
418 move |client| {
419 let launched_with = launched_with.clone();
420 client.on_request::<dap::requests::Launch, _>(move |_, args| {
421 launched_with.lock().replace(args.raw);
422 Ok(())
423 });
424 client.on_request::<dap::requests::Attach, _>(move |_, _| {
425 assert!(false, "should not get attach request");
426 Ok(())
427 });
428 }
429 });
430
431 let sessions = workspace
432 .update(cx, |workspace, _window, cx| {
433 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
434 debug_panel.read(cx).sessions().collect::<Vec<_>>()
435 })
436 .unwrap();
437 assert_eq!(sessions.len(), 1);
438 client
439 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
440 request: StartDebuggingRequestArgumentsRequest::Launch,
441 configuration: fake_config.clone(),
442 })
443 .await;
444
445 cx.run_until_parked();
446
447 workspace
448 .update(cx, |workspace, _window, cx| {
449 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
450
451 // Active session changes on spawn, as the parent has never stopped.
452 let active_session = debug_panel
453 .read(cx)
454 .active_session()
455 .unwrap()
456 .read(cx)
457 .session(cx);
458 let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
459 assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
460 assert_eq!(
461 active_session.read(cx).parent_session(),
462 Some(¤t_sessions[0].read(cx).session(cx))
463 );
464
465 assert_eq!(current_sessions.len(), 2);
466 assert_eq!(current_sessions[0], sessions[0]);
467
468 let parent_session = current_sessions[1]
469 .read(cx)
470 .session(cx)
471 .read(cx)
472 .parent_session()
473 .unwrap();
474 assert_eq!(parent_session, &sessions[0].read(cx).session(cx));
475
476 // We should preserve the original binary (params to spawn process etc.) except for launch params
477 // (as they come from reverse spawn request).
478 let mut original_binary = parent_session.read(cx).binary().cloned().unwrap();
479 original_binary.request_args = StartDebuggingRequestArguments {
480 request: StartDebuggingRequestArgumentsRequest::Launch,
481 configuration: fake_config.clone(),
482 };
483
484 assert_eq!(
485 current_sessions[1]
486 .read(cx)
487 .session(cx)
488 .read(cx)
489 .binary()
490 .unwrap(),
491 &original_binary
492 );
493 })
494 .unwrap();
495
496 assert_eq!(&fake_config, launched_with.lock().as_ref().unwrap());
497}
498
499// // covers that we always send a response back, if something when wrong,
500// // while spawning the terminal
501#[gpui::test]
502async fn test_handle_error_run_in_terminal_reverse_request(
503 executor: BackgroundExecutor,
504 cx: &mut TestAppContext,
505) {
506 init_test(cx);
507
508 let send_response = Arc::new(AtomicBool::new(false));
509
510 let fs = FakeFs::new(executor.clone());
511
512 fs.insert_tree(
513 path!("/project"),
514 json!({
515 "main.rs": "First line\nSecond line\nThird line\nFourth line",
516 }),
517 )
518 .await;
519
520 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
521 let workspace = init_test_workspace(&project, cx).await;
522 let cx = &mut VisualTestContext::from_window(*workspace, cx);
523
524 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
525 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
526
527 client
528 .on_response::<RunInTerminal, _>({
529 let send_response = send_response.clone();
530 move |response| {
531 send_response.store(true, Ordering::SeqCst);
532
533 assert!(!response.success);
534 assert!(response.body.is_some());
535 }
536 })
537 .await;
538
539 client
540 .fake_reverse_request::<RunInTerminal>(RunInTerminalRequestArguments {
541 kind: None,
542 title: None,
543 cwd: "".into(),
544 args: vec!["oops".into(), "oops".into()],
545 env: None,
546 args_can_be_interpreted_by_shell: None,
547 })
548 .await;
549
550 cx.run_until_parked();
551
552 assert!(
553 send_response.load(std::sync::atomic::Ordering::SeqCst),
554 "Expected to receive response from reverse request"
555 );
556
557 workspace
558 .update(cx, |workspace, _window, cx| {
559 let terminal_panel = workspace.panel::<TerminalPanel>(cx).unwrap();
560
561 assert_eq!(
562 0,
563 terminal_panel.read(cx).pane().unwrap().read(cx).items_len()
564 );
565 })
566 .unwrap();
567}
568
569#[gpui::test]
570async fn test_handle_start_debugging_reverse_request(
571 executor: BackgroundExecutor,
572 cx: &mut TestAppContext,
573) {
574 cx.executor().allow_parking();
575 init_test(cx);
576
577 let send_response = Arc::new(AtomicBool::new(false));
578
579 let fs = FakeFs::new(executor.clone());
580
581 fs.insert_tree(
582 path!("/project"),
583 json!({
584 "main.rs": "First line\nSecond line\nThird line\nFourth line",
585 }),
586 )
587 .await;
588
589 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
590 let workspace = init_test_workspace(&project, cx).await;
591 let cx = &mut VisualTestContext::from_window(*workspace, cx);
592
593 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
594 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
595
596 client.on_request::<dap::requests::Threads, _>(move |_, _| {
597 Ok(dap::ThreadsResponse {
598 threads: vec![dap::Thread {
599 id: 1,
600 name: "Thread 1".into(),
601 }],
602 })
603 });
604
605 client
606 .on_response::<StartDebugging, _>({
607 let send_response = send_response.clone();
608 move |response| {
609 send_response.store(true, Ordering::SeqCst);
610
611 assert!(response.success);
612 assert!(response.body.is_some());
613 }
614 })
615 .await;
616 // Set up handlers for sessions spawned with reverse request too.
617 let _reverse_request_subscription =
618 project::debugger::test::intercept_debug_sessions(cx, |_| {});
619 client
620 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
621 configuration: json!({}),
622 request: StartDebuggingRequestArgumentsRequest::Launch,
623 })
624 .await;
625
626 cx.run_until_parked();
627
628 let child_session = project.update(cx, |project, cx| {
629 project
630 .dap_store()
631 .read(cx)
632 .session_by_id(SessionId(1))
633 .unwrap()
634 });
635 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
636
637 child_client.on_request::<dap::requests::Threads, _>(move |_, _| {
638 Ok(dap::ThreadsResponse {
639 threads: vec![dap::Thread {
640 id: 1,
641 name: "Thread 1".into(),
642 }],
643 })
644 });
645
646 child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
647
648 child_client
649 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
650 reason: dap::StoppedEventReason::Pause,
651 description: None,
652 thread_id: Some(2),
653 preserve_focus_hint: None,
654 text: None,
655 all_threads_stopped: None,
656 hit_breakpoint_ids: None,
657 }))
658 .await;
659
660 cx.run_until_parked();
661
662 assert!(
663 send_response.load(std::sync::atomic::Ordering::SeqCst),
664 "Expected to receive response from reverse request"
665 );
666}
667
668#[gpui::test]
669async fn test_shutdown_children_when_parent_session_shutdown(
670 executor: BackgroundExecutor,
671 cx: &mut TestAppContext,
672) {
673 init_test(cx);
674
675 let fs = FakeFs::new(executor.clone());
676
677 fs.insert_tree(
678 path!("/project"),
679 json!({
680 "main.rs": "First line\nSecond line\nThird line\nFourth line",
681 }),
682 )
683 .await;
684
685 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
686 let dap_store = project.update(cx, |project, _| project.dap_store());
687 let workspace = init_test_workspace(&project, cx).await;
688 let cx = &mut VisualTestContext::from_window(*workspace, cx);
689
690 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
691 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
692
693 client.on_request::<dap::requests::Threads, _>(move |_, _| {
694 Ok(dap::ThreadsResponse {
695 threads: vec![dap::Thread {
696 id: 1,
697 name: "Thread 1".into(),
698 }],
699 })
700 });
701
702 client.on_response::<StartDebugging, _>(move |_| {}).await;
703 // Set up handlers for sessions spawned with reverse request too.
704 let _reverse_request_subscription =
705 project::debugger::test::intercept_debug_sessions(cx, |_| {});
706 // start first child session
707 client
708 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
709 configuration: json!({}),
710 request: StartDebuggingRequestArgumentsRequest::Launch,
711 })
712 .await;
713
714 cx.run_until_parked();
715
716 // start second child session
717 client
718 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
719 configuration: json!({}),
720 request: StartDebuggingRequestArgumentsRequest::Launch,
721 })
722 .await;
723
724 cx.run_until_parked();
725
726 // configure first child session
727 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
728 dap_store.session_by_id(SessionId(1)).unwrap()
729 });
730 let first_child_client =
731 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
732
733 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
734
735 // configure second child session
736 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
737 dap_store.session_by_id(SessionId(2)).unwrap()
738 });
739 let second_child_client =
740 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
741
742 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
743
744 cx.run_until_parked();
745
746 // shutdown parent session
747 dap_store
748 .update(cx, |dap_store, cx| {
749 dap_store.shutdown_session(parent_session.read(cx).session_id(), cx)
750 })
751 .await
752 .unwrap();
753
754 // assert parent session and all children sessions are shutdown
755 dap_store.update(cx, |dap_store, cx| {
756 assert!(
757 dap_store
758 .session_by_id(parent_session.read(cx).session_id())
759 .is_none()
760 );
761 assert!(
762 dap_store
763 .session_by_id(first_child_session.read(cx).session_id())
764 .is_none()
765 );
766 assert!(
767 dap_store
768 .session_by_id(second_child_session.read(cx).session_id())
769 .is_none()
770 );
771 });
772}
773
774#[gpui::test]
775async fn test_shutdown_parent_session_if_all_children_are_shutdown(
776 executor: BackgroundExecutor,
777 cx: &mut TestAppContext,
778) {
779 init_test(cx);
780
781 let fs = FakeFs::new(executor.clone());
782
783 fs.insert_tree(
784 path!("/project"),
785 json!({
786 "main.rs": "First line\nSecond line\nThird line\nFourth line",
787 }),
788 )
789 .await;
790
791 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
792 let dap_store = project.update(cx, |project, _| project.dap_store());
793 let workspace = init_test_workspace(&project, cx).await;
794 let cx = &mut VisualTestContext::from_window(*workspace, cx);
795
796 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
797 let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
798
799 client.on_response::<StartDebugging, _>(move |_| {}).await;
800 // Set up handlers for sessions spawned with reverse request too.
801 let _reverse_request_subscription =
802 project::debugger::test::intercept_debug_sessions(cx, |_| {});
803 // start first child session
804 client
805 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
806 configuration: json!({}),
807 request: StartDebuggingRequestArgumentsRequest::Launch,
808 })
809 .await;
810
811 cx.run_until_parked();
812
813 // start second child session
814 client
815 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
816 configuration: json!({}),
817 request: StartDebuggingRequestArgumentsRequest::Launch,
818 })
819 .await;
820
821 cx.run_until_parked();
822
823 // configure first child session
824 let first_child_session = dap_store.read_with(cx, |dap_store, _| {
825 dap_store.session_by_id(SessionId(1)).unwrap()
826 });
827 let first_child_client =
828 first_child_session.update(cx, |session, _| session.adapter_client().unwrap());
829
830 first_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
831
832 // configure second child session
833 let second_child_session = dap_store.read_with(cx, |dap_store, _| {
834 dap_store.session_by_id(SessionId(2)).unwrap()
835 });
836 let second_child_client =
837 second_child_session.update(cx, |session, _| session.adapter_client().unwrap());
838
839 second_child_client.on_request::<Disconnect, _>(move |_, _| Ok(()));
840
841 cx.run_until_parked();
842
843 // shutdown first child session
844 dap_store
845 .update(cx, |dap_store, cx| {
846 dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx)
847 })
848 .await
849 .unwrap();
850
851 // assert parent session and second child session still exist
852 dap_store.update(cx, |dap_store, cx| {
853 assert!(
854 dap_store
855 .session_by_id(parent_session.read(cx).session_id())
856 .is_some()
857 );
858 assert!(
859 dap_store
860 .session_by_id(first_child_session.read(cx).session_id())
861 .is_none()
862 );
863 assert!(
864 dap_store
865 .session_by_id(second_child_session.read(cx).session_id())
866 .is_some()
867 );
868 });
869
870 // shutdown first child session
871 dap_store
872 .update(cx, |dap_store, cx| {
873 dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx)
874 })
875 .await
876 .unwrap();
877
878 // assert parent session got shutdown by second child session
879 // because it was the last child
880 dap_store.update(cx, |dap_store, cx| {
881 assert!(
882 dap_store
883 .session_by_id(parent_session.read(cx).session_id())
884 .is_none()
885 );
886 assert!(
887 dap_store
888 .session_by_id(second_child_session.read(cx).session_id())
889 .is_none()
890 );
891 });
892}
893
894#[gpui::test]
895async fn test_debug_panel_item_thread_status_reset_on_failure(
896 executor: BackgroundExecutor,
897 cx: &mut TestAppContext,
898) {
899 init_test(cx);
900
901 let fs = FakeFs::new(executor.clone());
902
903 fs.insert_tree(
904 path!("/project"),
905 json!({
906 "main.rs": "First line\nSecond line\nThird line\nFourth line",
907 }),
908 )
909 .await;
910
911 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
912 let workspace = init_test_workspace(&project, cx).await;
913 let cx = &mut VisualTestContext::from_window(*workspace, cx);
914
915 let session = start_debug_session(&workspace, cx, |client| {
916 client.on_request::<dap::requests::Initialize, _>(move |_, _| {
917 Ok(dap::Capabilities {
918 supports_step_back: Some(true),
919 ..Default::default()
920 })
921 });
922 })
923 .unwrap();
924
925 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
926 const THREAD_ID_NUM: i64 = 1;
927
928 client.on_request::<dap::requests::Threads, _>(move |_, _| {
929 Ok(dap::ThreadsResponse {
930 threads: vec![dap::Thread {
931 id: THREAD_ID_NUM,
932 name: "Thread 1".into(),
933 }],
934 })
935 });
936
937 client.on_request::<Launch, _>(move |_, _| Ok(()));
938
939 client.on_request::<StackTrace, _>(move |_, _| {
940 Ok(dap::StackTraceResponse {
941 stack_frames: Vec::default(),
942 total_frames: None,
943 })
944 });
945
946 client.on_request::<Next, _>(move |_, _| {
947 Err(ErrorResponse {
948 error: Some(dap::Message {
949 id: 1,
950 format: "error".into(),
951 variables: None,
952 send_telemetry: None,
953 show_user: None,
954 url: None,
955 url_label: None,
956 }),
957 })
958 });
959
960 client.on_request::<StepOut, _>(move |_, _| {
961 Err(ErrorResponse {
962 error: Some(dap::Message {
963 id: 1,
964 format: "error".into(),
965 variables: None,
966 send_telemetry: None,
967 show_user: None,
968 url: None,
969 url_label: None,
970 }),
971 })
972 });
973
974 client.on_request::<StepIn, _>(move |_, _| {
975 Err(ErrorResponse {
976 error: Some(dap::Message {
977 id: 1,
978 format: "error".into(),
979 variables: None,
980 send_telemetry: None,
981 show_user: None,
982 url: None,
983 url_label: None,
984 }),
985 })
986 });
987
988 client.on_request::<StepBack, _>(move |_, _| {
989 Err(ErrorResponse {
990 error: Some(dap::Message {
991 id: 1,
992 format: "error".into(),
993 variables: None,
994 send_telemetry: None,
995 show_user: None,
996 url: None,
997 url_label: None,
998 }),
999 })
1000 });
1001
1002 client.on_request::<Continue, _>(move |_, _| {
1003 Err(ErrorResponse {
1004 error: Some(dap::Message {
1005 id: 1,
1006 format: "error".into(),
1007 variables: None,
1008 send_telemetry: None,
1009 show_user: None,
1010 url: None,
1011 url_label: None,
1012 }),
1013 })
1014 });
1015
1016 client
1017 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1018 reason: dap::StoppedEventReason::Pause,
1019 description: None,
1020 thread_id: Some(1),
1021 preserve_focus_hint: None,
1022 text: None,
1023 all_threads_stopped: None,
1024 hit_breakpoint_ids: None,
1025 }))
1026 .await;
1027
1028 cx.run_until_parked();
1029
1030 let running_state = active_debug_session_panel(workspace, cx)
1031 .read_with(cx, |item, _| item.running_state().clone());
1032
1033 cx.run_until_parked();
1034 let thread_id = ThreadId(1);
1035
1036 for operation in &[
1037 "step_over",
1038 "continue_thread",
1039 "step_back",
1040 "step_in",
1041 "step_out",
1042 ] {
1043 running_state.update(cx, |running_state, cx| match *operation {
1044 "step_over" => running_state.step_over(cx),
1045 "continue_thread" => running_state.continue_thread(cx),
1046 "step_back" => running_state.step_back(cx),
1047 "step_in" => running_state.step_in(cx),
1048 "step_out" => running_state.step_out(cx),
1049 _ => unreachable!(),
1050 });
1051
1052 // Check that we step the thread status to the correct intermediate state
1053 running_state.update(cx, |running_state, cx| {
1054 assert_eq!(
1055 running_state
1056 .thread_status(cx)
1057 .expect("There should be an active thread selected"),
1058 match *operation {
1059 "continue_thread" => ThreadStatus::Running,
1060 _ => ThreadStatus::Stepping,
1061 },
1062 "Thread status was not set to correct intermediate state after {} request",
1063 operation
1064 );
1065 });
1066
1067 cx.run_until_parked();
1068
1069 running_state.update(cx, |running_state, cx| {
1070 assert_eq!(
1071 running_state
1072 .thread_status(cx)
1073 .expect("There should be an active thread selected"),
1074 ThreadStatus::Stopped,
1075 "Thread status not reset to Stopped after failed {}",
1076 operation
1077 );
1078
1079 // update state to running, so we can test it actually changes the status back to stopped
1080 running_state
1081 .session()
1082 .update(cx, |session, cx| session.continue_thread(thread_id, cx));
1083 });
1084 }
1085}
1086
1087#[gpui::test]
1088async fn test_send_breakpoints_when_editor_has_been_saved(
1089 executor: BackgroundExecutor,
1090 cx: &mut TestAppContext,
1091) {
1092 init_test(cx);
1093
1094 let fs = FakeFs::new(executor.clone());
1095
1096 fs.insert_tree(
1097 path!("/project"),
1098 json!({
1099 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1100 }),
1101 )
1102 .await;
1103
1104 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1105 let workspace = init_test_workspace(&project, cx).await;
1106 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1107 let project_path = Path::new(path!("/project"));
1108 let worktree = project
1109 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1110 .expect("This worktree should exist in project")
1111 .0;
1112
1113 let worktree_id = workspace
1114 .update(cx, |_, _, cx| worktree.read(cx).id())
1115 .unwrap();
1116
1117 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1118 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1119
1120 let buffer = project
1121 .update(cx, |project, cx| {
1122 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1123 })
1124 .await
1125 .unwrap();
1126
1127 let (editor, cx) = cx.add_window_view(|window, cx| {
1128 Editor::new(
1129 EditorMode::full(),
1130 MultiBuffer::build_from_buffer(buffer, cx),
1131 Some(project.clone()),
1132 window,
1133 cx,
1134 )
1135 });
1136
1137 client.on_request::<Launch, _>(move |_, _| Ok(()));
1138
1139 client.on_request::<StackTrace, _>(move |_, _| {
1140 Ok(dap::StackTraceResponse {
1141 stack_frames: Vec::default(),
1142 total_frames: None,
1143 })
1144 });
1145
1146 client
1147 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1148 reason: dap::StoppedEventReason::Pause,
1149 description: None,
1150 thread_id: Some(1),
1151 preserve_focus_hint: None,
1152 text: None,
1153 all_threads_stopped: None,
1154 hit_breakpoint_ids: None,
1155 }))
1156 .await;
1157
1158 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1159 client.on_request::<SetBreakpoints, _>({
1160 let called_set_breakpoints = called_set_breakpoints.clone();
1161 move |_, args| {
1162 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1163 assert_eq!(
1164 vec![SourceBreakpoint {
1165 line: 2,
1166 column: None,
1167 condition: None,
1168 hit_condition: None,
1169 log_message: None,
1170 mode: None
1171 }],
1172 args.breakpoints.unwrap()
1173 );
1174 assert!(!args.source_modified.unwrap());
1175
1176 called_set_breakpoints.store(true, Ordering::SeqCst);
1177
1178 Ok(dap::SetBreakpointsResponse {
1179 breakpoints: Vec::default(),
1180 })
1181 }
1182 });
1183
1184 editor.update_in(cx, |editor, window, cx| {
1185 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1186 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1187 });
1188
1189 cx.run_until_parked();
1190
1191 assert!(
1192 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1193 "SetBreakpoint request must be called"
1194 );
1195
1196 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1197 client.on_request::<SetBreakpoints, _>({
1198 let called_set_breakpoints = called_set_breakpoints.clone();
1199 move |_, args| {
1200 assert_eq!(path!("/project/main.rs"), args.source.path.unwrap());
1201 assert_eq!(
1202 vec![SourceBreakpoint {
1203 line: 3,
1204 column: None,
1205 condition: None,
1206 hit_condition: None,
1207 log_message: None,
1208 mode: None
1209 }],
1210 args.breakpoints.unwrap()
1211 );
1212 assert!(args.source_modified.unwrap());
1213
1214 called_set_breakpoints.store(true, Ordering::SeqCst);
1215
1216 Ok(dap::SetBreakpointsResponse {
1217 breakpoints: Vec::default(),
1218 })
1219 }
1220 });
1221
1222 editor.update_in(cx, |editor, window, cx| {
1223 editor.move_up(&zed_actions::editor::MoveUp, window, cx);
1224 editor.insert("new text\n", window, cx);
1225 });
1226
1227 editor
1228 .update_in(cx, |editor, window, cx| {
1229 editor.save(
1230 SaveOptions {
1231 format: true,
1232 force_format: false,
1233 autosave: false,
1234 },
1235 project.clone(),
1236 window,
1237 cx,
1238 )
1239 })
1240 .await
1241 .unwrap();
1242
1243 cx.run_until_parked();
1244
1245 assert!(
1246 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1247 "SetBreakpoint request must be called after editor is saved"
1248 );
1249}
1250
1251#[gpui::test]
1252async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1253 executor: BackgroundExecutor,
1254 cx: &mut TestAppContext,
1255) {
1256 init_test(cx);
1257
1258 let fs = FakeFs::new(executor.clone());
1259
1260 fs.insert_tree(
1261 path!("/project"),
1262 json!({
1263 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1264 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1265 "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1266 }),
1267 )
1268 .await;
1269
1270 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1271 let workspace = init_test_workspace(&project, cx).await;
1272 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1273 let project_path = Path::new(path!("/project"));
1274 let worktree = project
1275 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1276 .expect("This worktree should exist in project")
1277 .0;
1278
1279 let worktree_id = workspace
1280 .update(cx, |_, _, cx| worktree.read(cx).id())
1281 .unwrap();
1282
1283 let first = project
1284 .update(cx, |project, cx| {
1285 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1286 })
1287 .await
1288 .unwrap();
1289
1290 let second = project
1291 .update(cx, |project, cx| {
1292 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1293 })
1294 .await
1295 .unwrap();
1296
1297 let (first_editor, cx) = cx.add_window_view(|window, cx| {
1298 Editor::new(
1299 EditorMode::full(),
1300 MultiBuffer::build_from_buffer(first, cx),
1301 Some(project.clone()),
1302 window,
1303 cx,
1304 )
1305 });
1306
1307 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1308 Editor::new(
1309 EditorMode::full(),
1310 MultiBuffer::build_from_buffer(second, cx),
1311 Some(project.clone()),
1312 window,
1313 cx,
1314 )
1315 });
1316
1317 first_editor.update_in(cx, |editor, window, cx| {
1318 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1319 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1320 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1321 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1322 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1323 });
1324
1325 second_editor.update_in(cx, |editor, window, cx| {
1326 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1327 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1328 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1329 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1330 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1331 });
1332
1333 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1334 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1335
1336 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1337
1338 client.on_request::<SetBreakpoints, _>({
1339 move |_, args| {
1340 assert!(
1341 args.breakpoints.is_none_or(|bps| bps.is_empty()),
1342 "Send empty breakpoint sets to clear them from DAP servers"
1343 );
1344
1345 match args
1346 .source
1347 .path
1348 .expect("We should always send a breakpoint's path")
1349 .as_str()
1350 {
1351 path!("/project/main.rs") | path!("/project/second.rs") => {}
1352 _ => {
1353 panic!("Unset breakpoints for path that doesn't have any")
1354 }
1355 }
1356
1357 called_set_breakpoints.store(true, Ordering::SeqCst);
1358
1359 Ok(dap::SetBreakpointsResponse {
1360 breakpoints: Vec::default(),
1361 })
1362 }
1363 });
1364
1365 cx.dispatch_action(crate::ClearAllBreakpoints);
1366 cx.run_until_parked();
1367}
1368
1369#[gpui::test]
1370async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1371 executor: BackgroundExecutor,
1372 cx: &mut TestAppContext,
1373) {
1374 init_test(cx);
1375
1376 let fs = FakeFs::new(executor.clone());
1377
1378 fs.insert_tree(
1379 path!("/project"),
1380 json!({
1381 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1382 }),
1383 )
1384 .await;
1385
1386 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1387 let workspace = init_test_workspace(&project, cx).await;
1388 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1389
1390 start_debug_session(&workspace, cx, |client| {
1391 client.on_request::<dap::requests::Initialize, _>(|_, _| {
1392 Err(ErrorResponse {
1393 error: Some(Message {
1394 format: "failed to launch".to_string(),
1395 id: 1,
1396 variables: None,
1397 send_telemetry: None,
1398 show_user: None,
1399 url: None,
1400 url_label: None,
1401 }),
1402 })
1403 });
1404 })
1405 .ok();
1406
1407 cx.run_until_parked();
1408
1409 project.update(cx, |project, cx| {
1410 assert!(
1411 project.dap_store().read(cx).sessions().count() == 0,
1412 "Session wouldn't exist if it was shutdown"
1413 );
1414 });
1415}
1416
1417#[gpui::test]
1418async fn test_we_send_arguments_from_user_config(
1419 executor: BackgroundExecutor,
1420 cx: &mut TestAppContext,
1421) {
1422 init_test(cx);
1423
1424 let fs = FakeFs::new(executor.clone());
1425
1426 fs.insert_tree(
1427 path!("/project"),
1428 json!({
1429 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1430 }),
1431 )
1432 .await;
1433
1434 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1435 let workspace = init_test_workspace(&project, cx).await;
1436 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1437 let debug_definition = DebugTaskDefinition {
1438 adapter: "fake-adapter".into(),
1439 config: json!({
1440 "request": "launch",
1441 "program": "main.rs".to_owned(),
1442 "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1443 "cwd": path!("/Random_path"),
1444 "env": json!({ "KEY": "VALUE" }),
1445 }),
1446 label: "test".into(),
1447 tcp_connection: None,
1448 };
1449
1450 let launch_handler_called = Arc::new(AtomicBool::new(false));
1451
1452 start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1453 let launch_handler_called = launch_handler_called.clone();
1454
1455 move |client| {
1456 let debug_definition = debug_definition.clone();
1457 let launch_handler_called = launch_handler_called.clone();
1458
1459 client.on_request::<dap::requests::Launch, _>(move |_, args| {
1460 launch_handler_called.store(true, Ordering::SeqCst);
1461
1462 assert_eq!(args.raw, debug_definition.config);
1463
1464 Ok(())
1465 });
1466 }
1467 })
1468 .ok();
1469
1470 cx.run_until_parked();
1471
1472 assert!(
1473 launch_handler_called.load(Ordering::SeqCst),
1474 "Launch request handler was not called"
1475 );
1476}
1477
1478#[gpui::test]
1479async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1480 init_test(cx);
1481
1482 let fs = FakeFs::new(executor.clone());
1483
1484 fs.insert_tree(
1485 path!("/project"),
1486 json!({
1487 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1488 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1489 }),
1490 )
1491 .await;
1492
1493 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1494 let workspace = init_test_workspace(&project, cx).await;
1495 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1496 let project_path = Path::new(path!("/project"));
1497 let worktree = project
1498 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1499 .expect("This worktree should exist in project")
1500 .0;
1501
1502 let worktree_id = workspace
1503 .update(cx, |_, _, cx| worktree.read(cx).id())
1504 .unwrap();
1505
1506 let main_buffer = project
1507 .update(cx, |project, cx| {
1508 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1509 })
1510 .await
1511 .unwrap();
1512
1513 let second_buffer = project
1514 .update(cx, |project, cx| {
1515 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1516 })
1517 .await
1518 .unwrap();
1519
1520 let (main_editor, cx) = cx.add_window_view(|window, cx| {
1521 Editor::new(
1522 EditorMode::full(),
1523 MultiBuffer::build_from_buffer(main_buffer, cx),
1524 Some(project.clone()),
1525 window,
1526 cx,
1527 )
1528 });
1529
1530 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1531 Editor::new(
1532 EditorMode::full(),
1533 MultiBuffer::build_from_buffer(second_buffer, cx),
1534 Some(project.clone()),
1535 window,
1536 cx,
1537 )
1538 });
1539
1540 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1541 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1542
1543 client.on_request::<dap::requests::Threads, _>(move |_, _| {
1544 Ok(dap::ThreadsResponse {
1545 threads: vec![dap::Thread {
1546 id: 1,
1547 name: "Thread 1".into(),
1548 }],
1549 })
1550 });
1551
1552 client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1553 Ok(dap::ScopesResponse {
1554 scopes: Vec::default(),
1555 })
1556 });
1557
1558 client.on_request::<StackTrace, _>(move |_, args| {
1559 assert_eq!(args.thread_id, 1);
1560
1561 Ok(dap::StackTraceResponse {
1562 stack_frames: vec![dap::StackFrame {
1563 id: 1,
1564 name: "frame 1".into(),
1565 source: Some(dap::Source {
1566 name: Some("main.rs".into()),
1567 path: Some(path!("/project/main.rs").into()),
1568 source_reference: None,
1569 presentation_hint: None,
1570 origin: None,
1571 sources: None,
1572 adapter_data: None,
1573 checksums: None,
1574 }),
1575 line: 2,
1576 column: 0,
1577 end_line: None,
1578 end_column: None,
1579 can_restart: None,
1580 instruction_pointer_reference: None,
1581 module_id: None,
1582 presentation_hint: None,
1583 }],
1584 total_frames: None,
1585 })
1586 });
1587
1588 client
1589 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1590 reason: dap::StoppedEventReason::Breakpoint,
1591 description: None,
1592 thread_id: Some(1),
1593 preserve_focus_hint: None,
1594 text: None,
1595 all_threads_stopped: None,
1596 hit_breakpoint_ids: None,
1597 }))
1598 .await;
1599
1600 cx.run_until_parked();
1601
1602 main_editor.update_in(cx, |editor, window, cx| {
1603 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1604
1605 assert_eq!(
1606 active_debug_lines.len(),
1607 1,
1608 "There should be only one active debug line"
1609 );
1610
1611 let point = editor
1612 .snapshot(window, cx)
1613 .buffer_snapshot()
1614 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1615
1616 assert_eq!(point.row, 1);
1617 });
1618
1619 second_editor.update(cx, |editor, _| {
1620 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1621
1622 assert!(
1623 active_debug_lines.is_empty(),
1624 "There shouldn't be any active debug lines"
1625 );
1626 });
1627
1628 let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1629 client.on_request::<StackTrace, _>({
1630 let handled_second_stacktrace = handled_second_stacktrace.clone();
1631 move |_, args| {
1632 handled_second_stacktrace.store(true, Ordering::SeqCst);
1633 assert_eq!(args.thread_id, 1);
1634
1635 Ok(dap::StackTraceResponse {
1636 stack_frames: vec![dap::StackFrame {
1637 id: 2,
1638 name: "frame 2".into(),
1639 source: Some(dap::Source {
1640 name: Some("second.rs".into()),
1641 path: Some(path!("/project/second.rs").into()),
1642 source_reference: None,
1643 presentation_hint: None,
1644 origin: None,
1645 sources: None,
1646 adapter_data: None,
1647 checksums: None,
1648 }),
1649 line: 3,
1650 column: 0,
1651 end_line: None,
1652 end_column: None,
1653 can_restart: None,
1654 instruction_pointer_reference: None,
1655 module_id: None,
1656 presentation_hint: None,
1657 }],
1658 total_frames: None,
1659 })
1660 }
1661 });
1662
1663 client
1664 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1665 reason: dap::StoppedEventReason::Breakpoint,
1666 description: None,
1667 thread_id: Some(1),
1668 preserve_focus_hint: None,
1669 text: None,
1670 all_threads_stopped: None,
1671 hit_breakpoint_ids: None,
1672 }))
1673 .await;
1674
1675 cx.run_until_parked();
1676
1677 second_editor.update_in(cx, |editor, window, cx| {
1678 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1679
1680 assert_eq!(
1681 active_debug_lines.len(),
1682 1,
1683 "There should be only one active debug line"
1684 );
1685
1686 let point = editor
1687 .snapshot(window, cx)
1688 .buffer_snapshot()
1689 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1690
1691 assert_eq!(point.row, 2);
1692 });
1693
1694 main_editor.update(cx, |editor, _| {
1695 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1696
1697 assert!(
1698 active_debug_lines.is_empty(),
1699 "There shouldn't be any active debug lines"
1700 );
1701 });
1702
1703 assert!(
1704 handled_second_stacktrace.load(Ordering::SeqCst),
1705 "Second stacktrace request handler was not called"
1706 );
1707
1708 client
1709 .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1710 thread_id: 0,
1711 all_threads_continued: Some(true),
1712 }))
1713 .await;
1714
1715 cx.run_until_parked();
1716
1717 second_editor.update(cx, |editor, _| {
1718 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1719
1720 assert!(
1721 active_debug_lines.is_empty(),
1722 "There shouldn't be any active debug lines"
1723 );
1724 });
1725
1726 main_editor.update(cx, |editor, _| {
1727 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1728
1729 assert!(
1730 active_debug_lines.is_empty(),
1731 "There shouldn't be any active debug lines"
1732 );
1733 });
1734
1735 // Clean up
1736 let shutdown_session = project.update(cx, |project, cx| {
1737 project.dap_store().update(cx, |dap_store, cx| {
1738 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1739 })
1740 });
1741
1742 shutdown_session.await.unwrap();
1743
1744 main_editor.update(cx, |editor, _| {
1745 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1746
1747 assert!(
1748 active_debug_lines.is_empty(),
1749 "There shouldn't be any active debug lines after session shutdown"
1750 );
1751 });
1752
1753 second_editor.update(cx, |editor, _| {
1754 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1755
1756 assert!(
1757 active_debug_lines.is_empty(),
1758 "There shouldn't be any active debug lines after session shutdown"
1759 );
1760 });
1761}
1762
1763#[gpui::test]
1764async fn test_debug_adapters_shutdown_on_app_quit(
1765 executor: BackgroundExecutor,
1766 cx: &mut TestAppContext,
1767) {
1768 init_test(cx);
1769
1770 let fs = FakeFs::new(executor.clone());
1771
1772 fs.insert_tree(
1773 path!("/project"),
1774 json!({
1775 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1776 }),
1777 )
1778 .await;
1779
1780 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1781 let workspace = init_test_workspace(&project, cx).await;
1782 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1783
1784 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1785 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1786
1787 let disconnect_request_received = Arc::new(AtomicBool::new(false));
1788 let disconnect_clone = disconnect_request_received.clone();
1789
1790 client.on_request::<Disconnect, _>(move |_, _| {
1791 disconnect_clone.store(true, Ordering::SeqCst);
1792 Ok(())
1793 });
1794
1795 executor.run_until_parked();
1796
1797 workspace
1798 .update(cx, |workspace, _, cx| {
1799 let panel = workspace.panel::<DebugPanel>(cx).unwrap();
1800 panel.read_with(cx, |panel, _| {
1801 assert!(
1802 panel.sessions().next().is_some(),
1803 "Debug session should be active"
1804 );
1805 });
1806 })
1807 .unwrap();
1808
1809 cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1810
1811 executor.run_until_parked();
1812
1813 assert!(
1814 disconnect_request_received.load(Ordering::SeqCst),
1815 "Disconnect request should have been sent to the adapter on app shutdown"
1816 );
1817}
1818
1819#[gpui::test]
1820async fn test_breakpoint_jumps_only_in_proper_split_view(
1821 executor: BackgroundExecutor,
1822 cx: &mut TestAppContext,
1823) {
1824 init_test(cx);
1825
1826 let fs = FakeFs::new(executor.clone());
1827
1828 fs.insert_tree(
1829 path!("/project"),
1830 json!({
1831 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1832 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1833 }),
1834 )
1835 .await;
1836
1837 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1838 let workspace = init_test_workspace(&project, cx).await;
1839 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1840
1841 let project_path = Path::new(path!("/project"));
1842 let worktree = project
1843 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1844 .expect("This worktree should exist in project")
1845 .0;
1846
1847 let worktree_id = workspace
1848 .update(cx, |_, _, cx| worktree.read(cx).id())
1849 .unwrap();
1850
1851 // Open main.rs in pane A (the initial pane)
1852 let pane_a = workspace
1853 .update(cx, |multi, _window, cx| {
1854 multi.workspace().read(cx).active_pane().clone()
1855 })
1856 .unwrap();
1857
1858 let open_main = workspace
1859 .update(cx, |multi, window, cx| {
1860 multi.workspace().update(cx, |workspace, cx| {
1861 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
1862 })
1863 })
1864 .unwrap();
1865 open_main.await.unwrap();
1866
1867 cx.run_until_parked();
1868
1869 // Split pane A to the right, creating pane B
1870 let pane_b = workspace
1871 .update(cx, |multi, window, cx| {
1872 multi.workspace().update(cx, |workspace, cx| {
1873 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
1874 })
1875 })
1876 .unwrap();
1877
1878 cx.run_until_parked();
1879
1880 // Open main.rs in pane B
1881 let weak_pane_b = pane_b.downgrade();
1882 let open_main_in_b = workspace
1883 .update(cx, |multi, window, cx| {
1884 multi.workspace().update(cx, |workspace, cx| {
1885 workspace.open_path(
1886 (worktree_id, rel_path("main.rs")),
1887 Some(weak_pane_b),
1888 true,
1889 window,
1890 cx,
1891 )
1892 })
1893 })
1894 .unwrap();
1895 open_main_in_b.await.unwrap();
1896
1897 cx.run_until_parked();
1898
1899 // Also open second.rs in pane B as an inactive tab
1900 let weak_pane_b = pane_b.downgrade();
1901 let open_second_in_b = workspace
1902 .update(cx, |multi, window, cx| {
1903 multi.workspace().update(cx, |workspace, cx| {
1904 workspace.open_path(
1905 (worktree_id, rel_path("second.rs")),
1906 Some(weak_pane_b),
1907 true,
1908 window,
1909 cx,
1910 )
1911 })
1912 })
1913 .unwrap();
1914 open_second_in_b.await.unwrap();
1915
1916 cx.run_until_parked();
1917
1918 // Switch pane B back to main.rs so second.rs is inactive there
1919 let weak_pane_b = pane_b.downgrade();
1920 let reactivate_main_in_b = workspace
1921 .update(cx, |multi, window, cx| {
1922 multi.workspace().update(cx, |workspace, cx| {
1923 workspace.open_path(
1924 (worktree_id, rel_path("main.rs")),
1925 Some(weak_pane_b),
1926 true,
1927 window,
1928 cx,
1929 )
1930 })
1931 })
1932 .unwrap();
1933 reactivate_main_in_b.await.unwrap();
1934
1935 cx.run_until_parked();
1936
1937 // Now open second.rs in pane A, making main.rs an inactive tab there
1938 let weak_pane_a = pane_a.downgrade();
1939 let open_second = workspace
1940 .update(cx, |multi, window, cx| {
1941 multi.workspace().update(cx, |workspace, cx| {
1942 workspace.open_path(
1943 (worktree_id, rel_path("second.rs")),
1944 Some(weak_pane_a),
1945 true,
1946 window,
1947 cx,
1948 )
1949 })
1950 })
1951 .unwrap();
1952 open_second.await.unwrap();
1953
1954 cx.run_until_parked();
1955
1956 // Layout:
1957 // Pane A: second.rs (active), main.rs (inactive tab)
1958 // Pane B: main.rs (active), second.rs (inactive tab)
1959
1960 // Verify pane A's active item is second.rs (main.rs is an inactive tab)
1961 workspace
1962 .read_with(cx, |_multi, cx| {
1963 let active = pane_a.read(cx).active_item().unwrap();
1964 let editor = active.to_any_view().downcast::<Editor>().unwrap();
1965 let path = editor.read(cx).project_path(cx).unwrap();
1966 assert_eq!(
1967 path.path.file_name().unwrap(),
1968 "second.rs",
1969 "Pane A should have second.rs active",
1970 );
1971 })
1972 .unwrap();
1973
1974 // Verify pane B's active item is main.rs
1975 workspace
1976 .read_with(cx, |_multi, cx| {
1977 let active = pane_b.read(cx).active_item().unwrap();
1978 let editor = active.to_any_view().downcast::<Editor>().unwrap();
1979 let path = editor.read(cx).project_path(cx).unwrap();
1980 assert_eq!(
1981 path.path.file_name().unwrap(),
1982 "main.rs",
1983 "Pane B should have main.rs active",
1984 );
1985 })
1986 .unwrap();
1987
1988 // Start a debug session and trigger a breakpoint stop on main.rs line 2
1989 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1990 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1991
1992 client.on_request::<Threads, _>(move |_, _| {
1993 Ok(dap::ThreadsResponse {
1994 threads: vec![dap::Thread {
1995 id: 1,
1996 name: "Thread 1".into(),
1997 }],
1998 })
1999 });
2000
2001 client.on_request::<dap::requests::Scopes, _>(move |_, _| {
2002 Ok(dap::ScopesResponse {
2003 scopes: Vec::default(),
2004 })
2005 });
2006
2007 client.on_request::<StackTrace, _>(move |_, args| {
2008 assert_eq!(args.thread_id, 1);
2009
2010 Ok(dap::StackTraceResponse {
2011 stack_frames: vec![dap::StackFrame {
2012 id: 1,
2013 name: "frame 1".into(),
2014 source: Some(dap::Source {
2015 name: Some("main.rs".into()),
2016 path: Some(path!("/project/main.rs").into()),
2017 source_reference: None,
2018 presentation_hint: None,
2019 origin: None,
2020 sources: None,
2021 adapter_data: None,
2022 checksums: None,
2023 }),
2024 line: 2,
2025 column: 0,
2026 end_line: None,
2027 end_column: None,
2028 can_restart: None,
2029 instruction_pointer_reference: None,
2030 module_id: None,
2031 presentation_hint: None,
2032 }],
2033 total_frames: None,
2034 })
2035 });
2036
2037 client
2038 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2039 reason: dap::StoppedEventReason::Breakpoint,
2040 description: None,
2041 thread_id: Some(1),
2042 preserve_focus_hint: None,
2043 text: None,
2044 all_threads_stopped: None,
2045 hit_breakpoint_ids: None,
2046 }))
2047 .await;
2048
2049 cx.run_until_parked();
2050
2051 // After first breakpoint stop on main.rs:
2052 // Pane A should still have second.rs as its active item because
2053 // main.rs was only an inactive tab there. The debugger should have jumped
2054 // to main.rs only in pane B where it was already the active tab.
2055 workspace
2056 .read_with(cx, |_multi, cx| {
2057 let pane_a_active = pane_a.read(cx).active_item().unwrap();
2058 let pane_a_editor = pane_a_active.to_any_view().downcast::<Editor>().unwrap();
2059 let pane_a_path = pane_a_editor.read(cx).project_path(cx).unwrap();
2060 assert_eq!(
2061 pane_a_path.path.file_name().unwrap(),
2062 "second.rs",
2063 "Pane A should still have second.rs as active item. \
2064 The debugger should not switch active tabs in panes where the \
2065 breakpoint file is not the active tab (issue #40602)",
2066 );
2067 })
2068 .unwrap();
2069
2070 // There should be exactly one active debug line across all editors in all panes
2071 workspace
2072 .read_with(cx, |_multi, cx| {
2073 let mut total_active_debug_lines = 0;
2074 for pane in [&pane_a, &pane_b] {
2075 for item in pane.read(cx).items() {
2076 if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2077 total_active_debug_lines += editor
2078 .read(cx)
2079 .highlighted_rows::<ActiveDebugLine>()
2080 .count();
2081 }
2082 }
2083 }
2084 assert_eq!(
2085 total_active_debug_lines, 1,
2086 "There should be exactly one active debug line across all editors in all panes"
2087 );
2088 })
2089 .unwrap();
2090
2091 // Pane B should show the debug highlight on main.rs
2092 workspace
2093 .read_with(cx, |_multi, cx| {
2094 let pane_b_active = pane_b.read(cx).active_item().unwrap();
2095 let pane_b_editor = pane_b_active.to_any_view().downcast::<Editor>().unwrap();
2096
2097 let active_debug_lines: Vec<_> = pane_b_editor
2098 .read(cx)
2099 .highlighted_rows::<ActiveDebugLine>()
2100 .collect();
2101
2102 assert_eq!(
2103 active_debug_lines.len(),
2104 1,
2105 "Pane B's main.rs editor should have the active debug line"
2106 );
2107 })
2108 .unwrap();
2109
2110 // Second breakpoint stop: now on second.rs line 3.
2111 // Even though pane A has second.rs as its active tab, the debug line
2112 // should open in pane B (the persistent debug pane) because pane B
2113 // had the last active debug line.
2114 client.on_request::<StackTrace, _>(move |_, args| {
2115 assert_eq!(args.thread_id, 1);
2116
2117 Ok(dap::StackTraceResponse {
2118 stack_frames: vec![dap::StackFrame {
2119 id: 2,
2120 name: "frame 2".into(),
2121 source: Some(dap::Source {
2122 name: Some("second.rs".into()),
2123 path: Some(path!("/project/second.rs").into()),
2124 source_reference: None,
2125 presentation_hint: None,
2126 origin: None,
2127 sources: None,
2128 adapter_data: None,
2129 checksums: None,
2130 }),
2131 line: 3,
2132 column: 0,
2133 end_line: None,
2134 end_column: None,
2135 can_restart: None,
2136 instruction_pointer_reference: None,
2137 module_id: None,
2138 presentation_hint: None,
2139 }],
2140 total_frames: None,
2141 })
2142 });
2143
2144 client
2145 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2146 reason: dap::StoppedEventReason::Breakpoint,
2147 description: None,
2148 thread_id: Some(1),
2149 preserve_focus_hint: None,
2150 text: None,
2151 all_threads_stopped: None,
2152 hit_breakpoint_ids: None,
2153 }))
2154 .await;
2155
2156 cx.run_until_parked();
2157
2158 // Pane B should now have second.rs as the active tab with the debug line,
2159 // because pane B was the last pane that had the debug line (persistent debug pane).
2160 workspace
2161 .read_with(cx, |_multi, cx| {
2162 let pane_b_active = pane_b.read(cx).active_item().unwrap();
2163 let pane_b_editor = pane_b_active.to_any_view().downcast::<Editor>().unwrap();
2164 let pane_b_path = pane_b_editor.read(cx).project_path(cx).unwrap();
2165 assert_eq!(
2166 pane_b_path.path.file_name().unwrap(),
2167 "second.rs",
2168 "Pane B should have switched to second.rs because it is the persistent debug pane",
2169 );
2170
2171 let active_debug_lines: Vec<_> = pane_b_editor
2172 .read(cx)
2173 .highlighted_rows::<ActiveDebugLine>()
2174 .collect();
2175
2176 assert_eq!(
2177 active_debug_lines.len(),
2178 1,
2179 "Pane B's second.rs editor should have the active debug line"
2180 );
2181 })
2182 .unwrap();
2183
2184 // There should still be exactly one active debug line across all editors
2185 workspace
2186 .read_with(cx, |_multi, cx| {
2187 let mut total_active_debug_lines = 0;
2188 for pane in [&pane_a, &pane_b] {
2189 for item in pane.read(cx).items() {
2190 if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2191 total_active_debug_lines += editor
2192 .read(cx)
2193 .highlighted_rows::<ActiveDebugLine>()
2194 .count();
2195 }
2196 }
2197 }
2198 assert_eq!(
2199 total_active_debug_lines, 1,
2200 "There should be exactly one active debug line across all editors after second stop"
2201 );
2202 })
2203 .unwrap();
2204
2205 // === New case: Move the debug pane (pane B) active item to a new pane C ===
2206 // This simulates a user dragging the tab with the active debug line to a new split.
2207 // The debugger should track that the debug line moved to pane C and use pane C
2208 // for subsequent debug stops.
2209
2210 // Split pane B to create pane C
2211 let pane_c = workspace
2212 .update(cx, |multi, window, cx| {
2213 multi.workspace().update(cx, |workspace, cx| {
2214 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
2215 })
2216 })
2217 .unwrap();
2218
2219 cx.run_until_parked();
2220
2221 // Move the active item (second.rs with debug line) from pane B to pane C
2222 workspace
2223 .update(cx, |_multi, window, cx| {
2224 move_active_item(&pane_b, &pane_c, true, false, window, cx);
2225 })
2226 .unwrap();
2227
2228 cx.run_until_parked();
2229
2230 // Verify pane C now has second.rs as active item
2231 workspace
2232 .read_with(cx, |_multi, cx| {
2233 let pane_c_active = pane_c.read(cx).active_item().unwrap();
2234 let pane_c_editor = pane_c_active.to_any_view().downcast::<Editor>().unwrap();
2235 let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap();
2236 assert_eq!(
2237 pane_c_path.path.file_name().unwrap(),
2238 "second.rs",
2239 "Pane C should have second.rs after moving it from pane B",
2240 );
2241 })
2242 .unwrap();
2243
2244 // Third breakpoint stop: back on main.rs line 2.
2245 // The debug line should appear in pane C because that's where the debug line
2246 // was moved to. The debugger should track pane moves.
2247 client.on_request::<StackTrace, _>(move |_, args| {
2248 assert_eq!(args.thread_id, 1);
2249
2250 Ok(dap::StackTraceResponse {
2251 stack_frames: vec![dap::StackFrame {
2252 id: 3,
2253 name: "frame 3".into(),
2254 source: Some(dap::Source {
2255 name: Some("main.rs".into()),
2256 path: Some(path!("/project/main.rs").into()),
2257 source_reference: None,
2258 presentation_hint: None,
2259 origin: None,
2260 sources: None,
2261 adapter_data: None,
2262 checksums: None,
2263 }),
2264 line: 2,
2265 column: 0,
2266 end_line: None,
2267 end_column: None,
2268 can_restart: None,
2269 instruction_pointer_reference: None,
2270 module_id: None,
2271 presentation_hint: None,
2272 }],
2273 total_frames: None,
2274 })
2275 });
2276
2277 client
2278 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2279 reason: dap::StoppedEventReason::Breakpoint,
2280 description: None,
2281 thread_id: Some(1),
2282 preserve_focus_hint: None,
2283 text: None,
2284 all_threads_stopped: None,
2285 hit_breakpoint_ids: None,
2286 }))
2287 .await;
2288
2289 cx.run_until_parked();
2290
2291 // Pane C should now have main.rs as the active tab with the debug line,
2292 // because pane C is where the debug line was moved to from pane B.
2293 workspace
2294 .read_with(cx, |_multi, cx| {
2295 let pane_c_active = pane_c.read(cx).active_item().unwrap();
2296 let pane_c_editor = pane_c_active.to_any_view().downcast::<Editor>().unwrap();
2297 let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap();
2298 assert_eq!(
2299 pane_c_path.path.file_name().unwrap(),
2300 "main.rs",
2301 "Pane C should have switched to main.rs because it is now the persistent debug pane \
2302 (the debug line was moved here from pane B)",
2303 );
2304
2305 let active_debug_lines: Vec<_> = pane_c_editor
2306 .read(cx)
2307 .highlighted_rows::<ActiveDebugLine>()
2308 .collect();
2309
2310 assert_eq!(
2311 active_debug_lines.len(),
2312 1,
2313 "Pane C's main.rs editor should have the active debug line"
2314 );
2315 })
2316 .unwrap();
2317
2318 // There should still be exactly one active debug line across all editors
2319 workspace
2320 .read_with(cx, |_multi, cx| {
2321 let mut total_active_debug_lines = 0;
2322 for pane in [&pane_a, &pane_b, &pane_c] {
2323 for item in pane.read(cx).items() {
2324 if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2325 total_active_debug_lines += editor
2326 .read(cx)
2327 .highlighted_rows::<ActiveDebugLine>()
2328 .count();
2329 }
2330 }
2331 }
2332 assert_eq!(
2333 total_active_debug_lines, 1,
2334 "There should be exactly one active debug line across all editors after third stop"
2335 );
2336 })
2337 .unwrap();
2338
2339 // Clean up
2340 let shutdown_session = project.update(cx, |project, cx| {
2341 project.dap_store().update(cx, |dap_store, cx| {
2342 dap_store.shutdown_session(session.read(cx).session_id(), cx)
2343 })
2344 });
2345
2346 shutdown_session.await.unwrap();
2347}
2348
2349#[gpui::test]
2350async fn test_adapter_shutdown_with_child_sessions_on_app_quit(
2351 executor: BackgroundExecutor,
2352 cx: &mut TestAppContext,
2353) {
2354 init_test(cx);
2355
2356 let fs = FakeFs::new(executor.clone());
2357
2358 fs.insert_tree(
2359 path!("/project"),
2360 json!({
2361 "main.rs": "First line\nSecond line\nThird line\nFourth line",
2362 }),
2363 )
2364 .await;
2365
2366 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
2367 let workspace = init_test_workspace(&project, cx).await;
2368 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2369
2370 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
2371 let parent_session_id = cx.read(|cx| parent_session.read(cx).session_id());
2372 let parent_client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
2373
2374 let disconnect_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2375 let parent_disconnect_called = Arc::new(AtomicBool::new(false));
2376 let parent_disconnect_clone = parent_disconnect_called.clone();
2377 let disconnect_count_clone = disconnect_count.clone();
2378
2379 parent_client.on_request::<Disconnect, _>(move |_, _| {
2380 parent_disconnect_clone.store(true, Ordering::SeqCst);
2381 disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
2382
2383 for _ in 0..50 {
2384 if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
2385 break;
2386 }
2387 std::thread::sleep(std::time::Duration::from_millis(1));
2388 }
2389
2390 Ok(())
2391 });
2392
2393 parent_client
2394 .on_response::<StartDebugging, _>(move |_| {})
2395 .await;
2396 let _subscription = project::debugger::test::intercept_debug_sessions(cx, |_| {});
2397
2398 parent_client
2399 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
2400 configuration: json!({}),
2401 request: StartDebuggingRequestArgumentsRequest::Launch,
2402 })
2403 .await;
2404
2405 cx.run_until_parked();
2406
2407 let child_session = project.update(cx, |project, cx| {
2408 project
2409 .dap_store()
2410 .read(cx)
2411 .session_by_id(SessionId(1))
2412 .unwrap()
2413 });
2414 let child_session_id = cx.read(|cx| child_session.read(cx).session_id());
2415 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
2416
2417 let child_disconnect_called = Arc::new(AtomicBool::new(false));
2418 let child_disconnect_clone = child_disconnect_called.clone();
2419 let disconnect_count_clone = disconnect_count.clone();
2420
2421 child_client.on_request::<Disconnect, _>(move |_, _| {
2422 child_disconnect_clone.store(true, Ordering::SeqCst);
2423 disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
2424
2425 for _ in 0..50 {
2426 if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
2427 break;
2428 }
2429 std::thread::sleep(std::time::Duration::from_millis(1));
2430 }
2431
2432 Ok(())
2433 });
2434
2435 executor.run_until_parked();
2436
2437 project.update(cx, |project, cx| {
2438 let store = project.dap_store().read(cx);
2439 assert!(store.session_by_id(parent_session_id).is_some());
2440 assert!(store.session_by_id(child_session_id).is_some());
2441 });
2442
2443 cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
2444
2445 executor.run_until_parked();
2446
2447 let parent_disconnect_check = parent_disconnect_called.clone();
2448 let child_disconnect_check = child_disconnect_called.clone();
2449 let executor_clone = executor.clone();
2450 let both_disconnected = executor
2451 .spawn(async move {
2452 let parent_disconnect = parent_disconnect_check;
2453 let child_disconnect = child_disconnect_check;
2454
2455 // We only have 100ms to shutdown the app
2456 for _ in 0..100 {
2457 if parent_disconnect.load(Ordering::SeqCst)
2458 && child_disconnect.load(Ordering::SeqCst)
2459 {
2460 return true;
2461 }
2462
2463 executor_clone
2464 .timer(std::time::Duration::from_millis(1))
2465 .await;
2466 }
2467
2468 false
2469 })
2470 .await;
2471
2472 assert!(
2473 both_disconnected,
2474 "Both parent and child sessions should receive disconnect requests"
2475 );
2476
2477 assert!(
2478 parent_disconnect_called.load(Ordering::SeqCst),
2479 "Parent session should have received disconnect request"
2480 );
2481 assert!(
2482 child_disconnect_called.load(Ordering::SeqCst),
2483 "Child session should have received disconnect request"
2484 );
2485}
2486
2487#[gpui::test]
2488async fn test_restart_request_is_not_sent_more_than_once_until_response(
2489 executor: BackgroundExecutor,
2490 cx: &mut TestAppContext,
2491) {
2492 init_test(cx);
2493
2494 let fs = FakeFs::new(executor.clone());
2495
2496 fs.insert_tree(
2497 path!("/project"),
2498 json!({
2499 "main.rs": "First line\nSecond line\nThird line\nFourth line",
2500 }),
2501 )
2502 .await;
2503
2504 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
2505 let workspace = init_test_workspace(&project, cx).await;
2506 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2507
2508 let session = start_debug_session(&workspace, cx, move |client| {
2509 client.on_request::<dap::requests::Initialize, _>(move |_, _| {
2510 Ok(dap::Capabilities {
2511 supports_restart_request: Some(true),
2512 ..Default::default()
2513 })
2514 });
2515 })
2516 .unwrap();
2517
2518 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
2519
2520 let restart_count = Arc::new(AtomicUsize::new(0));
2521
2522 client.on_request::<dap::requests::Restart, _>({
2523 let restart_count = restart_count.clone();
2524 move |_, _| {
2525 restart_count.fetch_add(1, Ordering::SeqCst);
2526 Ok(())
2527 }
2528 });
2529
2530 // This works because the restart request sender is on the foreground thread
2531 // so it will start running after the gpui update stack is cleared
2532 session.update(cx, |session, cx| {
2533 session.restart(None, cx);
2534 session.restart(None, cx);
2535 session.restart(None, cx);
2536 });
2537
2538 cx.run_until_parked();
2539
2540 assert_eq!(
2541 restart_count.load(Ordering::SeqCst),
2542 1,
2543 "Only one restart request should be sent while a restart is in-flight"
2544 );
2545
2546 session.update(cx, |session, cx| {
2547 session.restart(None, cx);
2548 });
2549
2550 cx.run_until_parked();
2551
2552 assert_eq!(
2553 restart_count.load(Ordering::SeqCst),
2554 2,
2555 "A second restart should be allowed after the first one completes"
2556 );
2557}