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 autosave: false,
1233 },
1234 project.clone(),
1235 window,
1236 cx,
1237 )
1238 })
1239 .await
1240 .unwrap();
1241
1242 cx.run_until_parked();
1243
1244 assert!(
1245 called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst),
1246 "SetBreakpoint request must be called after editor is saved"
1247 );
1248}
1249
1250#[gpui::test]
1251async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
1252 executor: BackgroundExecutor,
1253 cx: &mut TestAppContext,
1254) {
1255 init_test(cx);
1256
1257 let fs = FakeFs::new(executor.clone());
1258
1259 fs.insert_tree(
1260 path!("/project"),
1261 json!({
1262 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1263 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1264 "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints"
1265 }),
1266 )
1267 .await;
1268
1269 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1270 let workspace = init_test_workspace(&project, cx).await;
1271 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1272 let project_path = Path::new(path!("/project"));
1273 let worktree = project
1274 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1275 .expect("This worktree should exist in project")
1276 .0;
1277
1278 let worktree_id = workspace
1279 .update(cx, |_, _, cx| worktree.read(cx).id())
1280 .unwrap();
1281
1282 let first = project
1283 .update(cx, |project, cx| {
1284 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1285 })
1286 .await
1287 .unwrap();
1288
1289 let second = project
1290 .update(cx, |project, cx| {
1291 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1292 })
1293 .await
1294 .unwrap();
1295
1296 let (first_editor, cx) = cx.add_window_view(|window, cx| {
1297 Editor::new(
1298 EditorMode::full(),
1299 MultiBuffer::build_from_buffer(first, cx),
1300 Some(project.clone()),
1301 window,
1302 cx,
1303 )
1304 });
1305
1306 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1307 Editor::new(
1308 EditorMode::full(),
1309 MultiBuffer::build_from_buffer(second, cx),
1310 Some(project.clone()),
1311 window,
1312 cx,
1313 )
1314 });
1315
1316 first_editor.update_in(cx, |editor, window, cx| {
1317 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1318 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1319 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1320 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1321 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1322 });
1323
1324 second_editor.update_in(cx, |editor, window, cx| {
1325 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1326 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1327 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1328 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
1329 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
1330 });
1331
1332 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1333 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1334
1335 let called_set_breakpoints = Arc::new(AtomicBool::new(false));
1336
1337 client.on_request::<SetBreakpoints, _>({
1338 move |_, args| {
1339 assert!(
1340 args.breakpoints.is_none_or(|bps| bps.is_empty()),
1341 "Send empty breakpoint sets to clear them from DAP servers"
1342 );
1343
1344 match args
1345 .source
1346 .path
1347 .expect("We should always send a breakpoint's path")
1348 .as_str()
1349 {
1350 path!("/project/main.rs") | path!("/project/second.rs") => {}
1351 _ => {
1352 panic!("Unset breakpoints for path that doesn't have any")
1353 }
1354 }
1355
1356 called_set_breakpoints.store(true, Ordering::SeqCst);
1357
1358 Ok(dap::SetBreakpointsResponse {
1359 breakpoints: Vec::default(),
1360 })
1361 }
1362 });
1363
1364 cx.dispatch_action(crate::ClearAllBreakpoints);
1365 cx.run_until_parked();
1366}
1367
1368#[gpui::test]
1369async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails(
1370 executor: BackgroundExecutor,
1371 cx: &mut TestAppContext,
1372) {
1373 init_test(cx);
1374
1375 let fs = FakeFs::new(executor.clone());
1376
1377 fs.insert_tree(
1378 path!("/project"),
1379 json!({
1380 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1381 }),
1382 )
1383 .await;
1384
1385 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1386 let workspace = init_test_workspace(&project, cx).await;
1387 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1388
1389 start_debug_session(&workspace, cx, |client| {
1390 client.on_request::<dap::requests::Initialize, _>(|_, _| {
1391 Err(ErrorResponse {
1392 error: Some(Message {
1393 format: "failed to launch".to_string(),
1394 id: 1,
1395 variables: None,
1396 send_telemetry: None,
1397 show_user: None,
1398 url: None,
1399 url_label: None,
1400 }),
1401 })
1402 });
1403 })
1404 .ok();
1405
1406 cx.run_until_parked();
1407
1408 project.update(cx, |project, cx| {
1409 assert!(
1410 project.dap_store().read(cx).sessions().count() == 0,
1411 "Session wouldn't exist if it was shutdown"
1412 );
1413 });
1414}
1415
1416#[gpui::test]
1417async fn test_we_send_arguments_from_user_config(
1418 executor: BackgroundExecutor,
1419 cx: &mut TestAppContext,
1420) {
1421 init_test(cx);
1422
1423 let fs = FakeFs::new(executor.clone());
1424
1425 fs.insert_tree(
1426 path!("/project"),
1427 json!({
1428 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1429 }),
1430 )
1431 .await;
1432
1433 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1434 let workspace = init_test_workspace(&project, cx).await;
1435 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1436 let debug_definition = DebugTaskDefinition {
1437 adapter: "fake-adapter".into(),
1438 config: json!({
1439 "request": "launch",
1440 "program": "main.rs".to_owned(),
1441 "args": vec!["arg1".to_owned(), "arg2".to_owned()],
1442 "cwd": path!("/Random_path"),
1443 "env": json!({ "KEY": "VALUE" }),
1444 }),
1445 label: "test".into(),
1446 tcp_connection: None,
1447 };
1448
1449 let launch_handler_called = Arc::new(AtomicBool::new(false));
1450
1451 start_debug_session_with(&workspace, cx, debug_definition.clone(), {
1452 let launch_handler_called = launch_handler_called.clone();
1453
1454 move |client| {
1455 let debug_definition = debug_definition.clone();
1456 let launch_handler_called = launch_handler_called.clone();
1457
1458 client.on_request::<dap::requests::Launch, _>(move |_, args| {
1459 launch_handler_called.store(true, Ordering::SeqCst);
1460
1461 assert_eq!(args.raw, debug_definition.config);
1462
1463 Ok(())
1464 });
1465 }
1466 })
1467 .ok();
1468
1469 cx.run_until_parked();
1470
1471 assert!(
1472 launch_handler_called.load(Ordering::SeqCst),
1473 "Launch request handler was not called"
1474 );
1475}
1476
1477#[gpui::test]
1478async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut TestAppContext) {
1479 init_test(cx);
1480
1481 let fs = FakeFs::new(executor.clone());
1482
1483 fs.insert_tree(
1484 path!("/project"),
1485 json!({
1486 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1487 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1488 }),
1489 )
1490 .await;
1491
1492 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1493 let workspace = init_test_workspace(&project, cx).await;
1494 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1495 let project_path = Path::new(path!("/project"));
1496 let worktree = project
1497 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1498 .expect("This worktree should exist in project")
1499 .0;
1500
1501 let worktree_id = workspace
1502 .update(cx, |_, _, cx| worktree.read(cx).id())
1503 .unwrap();
1504
1505 let main_buffer = project
1506 .update(cx, |project, cx| {
1507 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
1508 })
1509 .await
1510 .unwrap();
1511
1512 let second_buffer = project
1513 .update(cx, |project, cx| {
1514 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
1515 })
1516 .await
1517 .unwrap();
1518
1519 let (main_editor, cx) = cx.add_window_view(|window, cx| {
1520 Editor::new(
1521 EditorMode::full(),
1522 MultiBuffer::build_from_buffer(main_buffer, cx),
1523 Some(project.clone()),
1524 window,
1525 cx,
1526 )
1527 });
1528
1529 let (second_editor, cx) = cx.add_window_view(|window, cx| {
1530 Editor::new(
1531 EditorMode::full(),
1532 MultiBuffer::build_from_buffer(second_buffer, cx),
1533 Some(project.clone()),
1534 window,
1535 cx,
1536 )
1537 });
1538
1539 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1540 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1541
1542 client.on_request::<dap::requests::Threads, _>(move |_, _| {
1543 Ok(dap::ThreadsResponse {
1544 threads: vec![dap::Thread {
1545 id: 1,
1546 name: "Thread 1".into(),
1547 }],
1548 })
1549 });
1550
1551 client.on_request::<dap::requests::Scopes, _>(move |_, _| {
1552 Ok(dap::ScopesResponse {
1553 scopes: Vec::default(),
1554 })
1555 });
1556
1557 client.on_request::<StackTrace, _>(move |_, args| {
1558 assert_eq!(args.thread_id, 1);
1559
1560 Ok(dap::StackTraceResponse {
1561 stack_frames: vec![dap::StackFrame {
1562 id: 1,
1563 name: "frame 1".into(),
1564 source: Some(dap::Source {
1565 name: Some("main.rs".into()),
1566 path: Some(path!("/project/main.rs").into()),
1567 source_reference: None,
1568 presentation_hint: None,
1569 origin: None,
1570 sources: None,
1571 adapter_data: None,
1572 checksums: None,
1573 }),
1574 line: 2,
1575 column: 0,
1576 end_line: None,
1577 end_column: None,
1578 can_restart: None,
1579 instruction_pointer_reference: None,
1580 module_id: None,
1581 presentation_hint: None,
1582 }],
1583 total_frames: None,
1584 })
1585 });
1586
1587 client
1588 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1589 reason: dap::StoppedEventReason::Breakpoint,
1590 description: None,
1591 thread_id: Some(1),
1592 preserve_focus_hint: None,
1593 text: None,
1594 all_threads_stopped: None,
1595 hit_breakpoint_ids: None,
1596 }))
1597 .await;
1598
1599 cx.run_until_parked();
1600
1601 main_editor.update_in(cx, |editor, window, cx| {
1602 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1603
1604 assert_eq!(
1605 active_debug_lines.len(),
1606 1,
1607 "There should be only one active debug line"
1608 );
1609
1610 let point = editor
1611 .snapshot(window, cx)
1612 .buffer_snapshot()
1613 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1614
1615 assert_eq!(point.row, 1);
1616 });
1617
1618 second_editor.update(cx, |editor, _| {
1619 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1620
1621 assert!(
1622 active_debug_lines.is_empty(),
1623 "There shouldn't be any active debug lines"
1624 );
1625 });
1626
1627 let handled_second_stacktrace = Arc::new(AtomicBool::new(false));
1628 client.on_request::<StackTrace, _>({
1629 let handled_second_stacktrace = handled_second_stacktrace.clone();
1630 move |_, args| {
1631 handled_second_stacktrace.store(true, Ordering::SeqCst);
1632 assert_eq!(args.thread_id, 1);
1633
1634 Ok(dap::StackTraceResponse {
1635 stack_frames: vec![dap::StackFrame {
1636 id: 2,
1637 name: "frame 2".into(),
1638 source: Some(dap::Source {
1639 name: Some("second.rs".into()),
1640 path: Some(path!("/project/second.rs").into()),
1641 source_reference: None,
1642 presentation_hint: None,
1643 origin: None,
1644 sources: None,
1645 adapter_data: None,
1646 checksums: None,
1647 }),
1648 line: 3,
1649 column: 0,
1650 end_line: None,
1651 end_column: None,
1652 can_restart: None,
1653 instruction_pointer_reference: None,
1654 module_id: None,
1655 presentation_hint: None,
1656 }],
1657 total_frames: None,
1658 })
1659 }
1660 });
1661
1662 client
1663 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
1664 reason: dap::StoppedEventReason::Breakpoint,
1665 description: None,
1666 thread_id: Some(1),
1667 preserve_focus_hint: None,
1668 text: None,
1669 all_threads_stopped: None,
1670 hit_breakpoint_ids: None,
1671 }))
1672 .await;
1673
1674 cx.run_until_parked();
1675
1676 second_editor.update_in(cx, |editor, window, cx| {
1677 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1678
1679 assert_eq!(
1680 active_debug_lines.len(),
1681 1,
1682 "There should be only one active debug line"
1683 );
1684
1685 let point = editor
1686 .snapshot(window, cx)
1687 .buffer_snapshot()
1688 .summary_for_anchor::<language::Point>(&active_debug_lines.first().unwrap().0.start);
1689
1690 assert_eq!(point.row, 2);
1691 });
1692
1693 main_editor.update(cx, |editor, _| {
1694 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1695
1696 assert!(
1697 active_debug_lines.is_empty(),
1698 "There shouldn't be any active debug lines"
1699 );
1700 });
1701
1702 assert!(
1703 handled_second_stacktrace.load(Ordering::SeqCst),
1704 "Second stacktrace request handler was not called"
1705 );
1706
1707 client
1708 .fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
1709 thread_id: 0,
1710 all_threads_continued: Some(true),
1711 }))
1712 .await;
1713
1714 cx.run_until_parked();
1715
1716 second_editor.update(cx, |editor, _| {
1717 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1718
1719 assert!(
1720 active_debug_lines.is_empty(),
1721 "There shouldn't be any active debug lines"
1722 );
1723 });
1724
1725 main_editor.update(cx, |editor, _| {
1726 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1727
1728 assert!(
1729 active_debug_lines.is_empty(),
1730 "There shouldn't be any active debug lines"
1731 );
1732 });
1733
1734 // Clean up
1735 let shutdown_session = project.update(cx, |project, cx| {
1736 project.dap_store().update(cx, |dap_store, cx| {
1737 dap_store.shutdown_session(session.read(cx).session_id(), cx)
1738 })
1739 });
1740
1741 shutdown_session.await.unwrap();
1742
1743 main_editor.update(cx, |editor, _| {
1744 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1745
1746 assert!(
1747 active_debug_lines.is_empty(),
1748 "There shouldn't be any active debug lines after session shutdown"
1749 );
1750 });
1751
1752 second_editor.update(cx, |editor, _| {
1753 let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
1754
1755 assert!(
1756 active_debug_lines.is_empty(),
1757 "There shouldn't be any active debug lines after session shutdown"
1758 );
1759 });
1760}
1761
1762#[gpui::test]
1763async fn test_debug_adapters_shutdown_on_app_quit(
1764 executor: BackgroundExecutor,
1765 cx: &mut TestAppContext,
1766) {
1767 init_test(cx);
1768
1769 let fs = FakeFs::new(executor.clone());
1770
1771 fs.insert_tree(
1772 path!("/project"),
1773 json!({
1774 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1775 }),
1776 )
1777 .await;
1778
1779 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1780 let workspace = init_test_workspace(&project, cx).await;
1781 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1782
1783 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1784 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1785
1786 let disconnect_request_received = Arc::new(AtomicBool::new(false));
1787 let disconnect_clone = disconnect_request_received.clone();
1788
1789 client.on_request::<Disconnect, _>(move |_, _| {
1790 disconnect_clone.store(true, Ordering::SeqCst);
1791 Ok(())
1792 });
1793
1794 executor.run_until_parked();
1795
1796 workspace
1797 .update(cx, |workspace, _, cx| {
1798 let panel = workspace.panel::<DebugPanel>(cx).unwrap();
1799 panel.read_with(cx, |panel, _| {
1800 assert!(
1801 panel.sessions().next().is_some(),
1802 "Debug session should be active"
1803 );
1804 });
1805 })
1806 .unwrap();
1807
1808 cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
1809
1810 executor.run_until_parked();
1811
1812 assert!(
1813 disconnect_request_received.load(Ordering::SeqCst),
1814 "Disconnect request should have been sent to the adapter on app shutdown"
1815 );
1816}
1817
1818#[gpui::test]
1819async fn test_breakpoint_jumps_only_in_proper_split_view(
1820 executor: BackgroundExecutor,
1821 cx: &mut TestAppContext,
1822) {
1823 init_test(cx);
1824
1825 let fs = FakeFs::new(executor.clone());
1826
1827 fs.insert_tree(
1828 path!("/project"),
1829 json!({
1830 "main.rs": "First line\nSecond line\nThird line\nFourth line",
1831 "second.rs": "First line\nSecond line\nThird line\nFourth line",
1832 }),
1833 )
1834 .await;
1835
1836 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
1837 let workspace = init_test_workspace(&project, cx).await;
1838 let cx = &mut VisualTestContext::from_window(*workspace, cx);
1839
1840 let project_path = Path::new(path!("/project"));
1841 let worktree = project
1842 .update(cx, |project, cx| project.find_worktree(project_path, cx))
1843 .expect("This worktree should exist in project")
1844 .0;
1845
1846 let worktree_id = workspace
1847 .update(cx, |_, _, cx| worktree.read(cx).id())
1848 .unwrap();
1849
1850 // Open main.rs in pane A (the initial pane)
1851 let pane_a = workspace
1852 .update(cx, |multi, _window, cx| {
1853 multi.workspace().read(cx).active_pane().clone()
1854 })
1855 .unwrap();
1856
1857 let open_main = workspace
1858 .update(cx, |multi, window, cx| {
1859 multi.workspace().update(cx, |workspace, cx| {
1860 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
1861 })
1862 })
1863 .unwrap();
1864 open_main.await.unwrap();
1865
1866 cx.run_until_parked();
1867
1868 // Split pane A to the right, creating pane B
1869 let pane_b = workspace
1870 .update(cx, |multi, window, cx| {
1871 multi.workspace().update(cx, |workspace, cx| {
1872 workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx)
1873 })
1874 })
1875 .unwrap();
1876
1877 cx.run_until_parked();
1878
1879 // Open main.rs in pane B
1880 let weak_pane_b = pane_b.downgrade();
1881 let open_main_in_b = workspace
1882 .update(cx, |multi, window, cx| {
1883 multi.workspace().update(cx, |workspace, cx| {
1884 workspace.open_path(
1885 (worktree_id, rel_path("main.rs")),
1886 Some(weak_pane_b),
1887 true,
1888 window,
1889 cx,
1890 )
1891 })
1892 })
1893 .unwrap();
1894 open_main_in_b.await.unwrap();
1895
1896 cx.run_until_parked();
1897
1898 // Also open second.rs in pane B as an inactive tab
1899 let weak_pane_b = pane_b.downgrade();
1900 let open_second_in_b = workspace
1901 .update(cx, |multi, window, cx| {
1902 multi.workspace().update(cx, |workspace, cx| {
1903 workspace.open_path(
1904 (worktree_id, rel_path("second.rs")),
1905 Some(weak_pane_b),
1906 true,
1907 window,
1908 cx,
1909 )
1910 })
1911 })
1912 .unwrap();
1913 open_second_in_b.await.unwrap();
1914
1915 cx.run_until_parked();
1916
1917 // Switch pane B back to main.rs so second.rs is inactive there
1918 let weak_pane_b = pane_b.downgrade();
1919 let reactivate_main_in_b = workspace
1920 .update(cx, |multi, window, cx| {
1921 multi.workspace().update(cx, |workspace, cx| {
1922 workspace.open_path(
1923 (worktree_id, rel_path("main.rs")),
1924 Some(weak_pane_b),
1925 true,
1926 window,
1927 cx,
1928 )
1929 })
1930 })
1931 .unwrap();
1932 reactivate_main_in_b.await.unwrap();
1933
1934 cx.run_until_parked();
1935
1936 // Now open second.rs in pane A, making main.rs an inactive tab there
1937 let weak_pane_a = pane_a.downgrade();
1938 let open_second = workspace
1939 .update(cx, |multi, window, cx| {
1940 multi.workspace().update(cx, |workspace, cx| {
1941 workspace.open_path(
1942 (worktree_id, rel_path("second.rs")),
1943 Some(weak_pane_a),
1944 true,
1945 window,
1946 cx,
1947 )
1948 })
1949 })
1950 .unwrap();
1951 open_second.await.unwrap();
1952
1953 cx.run_until_parked();
1954
1955 // Layout:
1956 // Pane A: second.rs (active), main.rs (inactive tab)
1957 // Pane B: main.rs (active), second.rs (inactive tab)
1958
1959 // Verify pane A's active item is second.rs (main.rs is an inactive tab)
1960 workspace
1961 .read_with(cx, |_multi, cx| {
1962 let active = pane_a.read(cx).active_item().unwrap();
1963 let editor = active.to_any_view().downcast::<Editor>().unwrap();
1964 let path = editor.read(cx).project_path(cx).unwrap();
1965 assert_eq!(
1966 path.path.file_name().unwrap(),
1967 "second.rs",
1968 "Pane A should have second.rs active",
1969 );
1970 })
1971 .unwrap();
1972
1973 // Verify pane B's active item is main.rs
1974 workspace
1975 .read_with(cx, |_multi, cx| {
1976 let active = pane_b.read(cx).active_item().unwrap();
1977 let editor = active.to_any_view().downcast::<Editor>().unwrap();
1978 let path = editor.read(cx).project_path(cx).unwrap();
1979 assert_eq!(
1980 path.path.file_name().unwrap(),
1981 "main.rs",
1982 "Pane B should have main.rs active",
1983 );
1984 })
1985 .unwrap();
1986
1987 // Start a debug session and trigger a breakpoint stop on main.rs line 2
1988 let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
1989 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
1990
1991 client.on_request::<Threads, _>(move |_, _| {
1992 Ok(dap::ThreadsResponse {
1993 threads: vec![dap::Thread {
1994 id: 1,
1995 name: "Thread 1".into(),
1996 }],
1997 })
1998 });
1999
2000 client.on_request::<dap::requests::Scopes, _>(move |_, _| {
2001 Ok(dap::ScopesResponse {
2002 scopes: Vec::default(),
2003 })
2004 });
2005
2006 client.on_request::<StackTrace, _>(move |_, args| {
2007 assert_eq!(args.thread_id, 1);
2008
2009 Ok(dap::StackTraceResponse {
2010 stack_frames: vec![dap::StackFrame {
2011 id: 1,
2012 name: "frame 1".into(),
2013 source: Some(dap::Source {
2014 name: Some("main.rs".into()),
2015 path: Some(path!("/project/main.rs").into()),
2016 source_reference: None,
2017 presentation_hint: None,
2018 origin: None,
2019 sources: None,
2020 adapter_data: None,
2021 checksums: None,
2022 }),
2023 line: 2,
2024 column: 0,
2025 end_line: None,
2026 end_column: None,
2027 can_restart: None,
2028 instruction_pointer_reference: None,
2029 module_id: None,
2030 presentation_hint: None,
2031 }],
2032 total_frames: None,
2033 })
2034 });
2035
2036 client
2037 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2038 reason: dap::StoppedEventReason::Breakpoint,
2039 description: None,
2040 thread_id: Some(1),
2041 preserve_focus_hint: None,
2042 text: None,
2043 all_threads_stopped: None,
2044 hit_breakpoint_ids: None,
2045 }))
2046 .await;
2047
2048 cx.run_until_parked();
2049
2050 // After first breakpoint stop on main.rs:
2051 // Pane A should still have second.rs as its active item because
2052 // main.rs was only an inactive tab there. The debugger should have jumped
2053 // to main.rs only in pane B where it was already the active tab.
2054 workspace
2055 .read_with(cx, |_multi, cx| {
2056 let pane_a_active = pane_a.read(cx).active_item().unwrap();
2057 let pane_a_editor = pane_a_active.to_any_view().downcast::<Editor>().unwrap();
2058 let pane_a_path = pane_a_editor.read(cx).project_path(cx).unwrap();
2059 assert_eq!(
2060 pane_a_path.path.file_name().unwrap(),
2061 "second.rs",
2062 "Pane A should still have second.rs as active item. \
2063 The debugger should not switch active tabs in panes where the \
2064 breakpoint file is not the active tab (issue #40602)",
2065 );
2066 })
2067 .unwrap();
2068
2069 // There should be exactly one active debug line across all editors in all panes
2070 workspace
2071 .read_with(cx, |_multi, cx| {
2072 let mut total_active_debug_lines = 0;
2073 for pane in [&pane_a, &pane_b] {
2074 for item in pane.read(cx).items() {
2075 if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2076 total_active_debug_lines += editor
2077 .read(cx)
2078 .highlighted_rows::<ActiveDebugLine>()
2079 .count();
2080 }
2081 }
2082 }
2083 assert_eq!(
2084 total_active_debug_lines, 1,
2085 "There should be exactly one active debug line across all editors in all panes"
2086 );
2087 })
2088 .unwrap();
2089
2090 // Pane B should show the debug highlight on main.rs
2091 workspace
2092 .read_with(cx, |_multi, cx| {
2093 let pane_b_active = pane_b.read(cx).active_item().unwrap();
2094 let pane_b_editor = pane_b_active.to_any_view().downcast::<Editor>().unwrap();
2095
2096 let active_debug_lines: Vec<_> = pane_b_editor
2097 .read(cx)
2098 .highlighted_rows::<ActiveDebugLine>()
2099 .collect();
2100
2101 assert_eq!(
2102 active_debug_lines.len(),
2103 1,
2104 "Pane B's main.rs editor should have the active debug line"
2105 );
2106 })
2107 .unwrap();
2108
2109 // Second breakpoint stop: now on second.rs line 3.
2110 // Even though pane A has second.rs as its active tab, the debug line
2111 // should open in pane B (the persistent debug pane) because pane B
2112 // had the last active debug line.
2113 client.on_request::<StackTrace, _>(move |_, args| {
2114 assert_eq!(args.thread_id, 1);
2115
2116 Ok(dap::StackTraceResponse {
2117 stack_frames: vec![dap::StackFrame {
2118 id: 2,
2119 name: "frame 2".into(),
2120 source: Some(dap::Source {
2121 name: Some("second.rs".into()),
2122 path: Some(path!("/project/second.rs").into()),
2123 source_reference: None,
2124 presentation_hint: None,
2125 origin: None,
2126 sources: None,
2127 adapter_data: None,
2128 checksums: None,
2129 }),
2130 line: 3,
2131 column: 0,
2132 end_line: None,
2133 end_column: None,
2134 can_restart: None,
2135 instruction_pointer_reference: None,
2136 module_id: None,
2137 presentation_hint: None,
2138 }],
2139 total_frames: None,
2140 })
2141 });
2142
2143 client
2144 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2145 reason: dap::StoppedEventReason::Breakpoint,
2146 description: None,
2147 thread_id: Some(1),
2148 preserve_focus_hint: None,
2149 text: None,
2150 all_threads_stopped: None,
2151 hit_breakpoint_ids: None,
2152 }))
2153 .await;
2154
2155 cx.run_until_parked();
2156
2157 // Pane B should now have second.rs as the active tab with the debug line,
2158 // because pane B was the last pane that had the debug line (persistent debug pane).
2159 workspace
2160 .read_with(cx, |_multi, cx| {
2161 let pane_b_active = pane_b.read(cx).active_item().unwrap();
2162 let pane_b_editor = pane_b_active.to_any_view().downcast::<Editor>().unwrap();
2163 let pane_b_path = pane_b_editor.read(cx).project_path(cx).unwrap();
2164 assert_eq!(
2165 pane_b_path.path.file_name().unwrap(),
2166 "second.rs",
2167 "Pane B should have switched to second.rs because it is the persistent debug pane",
2168 );
2169
2170 let active_debug_lines: Vec<_> = pane_b_editor
2171 .read(cx)
2172 .highlighted_rows::<ActiveDebugLine>()
2173 .collect();
2174
2175 assert_eq!(
2176 active_debug_lines.len(),
2177 1,
2178 "Pane B's second.rs editor should have the active debug line"
2179 );
2180 })
2181 .unwrap();
2182
2183 // There should still be exactly one active debug line across all editors
2184 workspace
2185 .read_with(cx, |_multi, cx| {
2186 let mut total_active_debug_lines = 0;
2187 for pane in [&pane_a, &pane_b] {
2188 for item in pane.read(cx).items() {
2189 if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2190 total_active_debug_lines += editor
2191 .read(cx)
2192 .highlighted_rows::<ActiveDebugLine>()
2193 .count();
2194 }
2195 }
2196 }
2197 assert_eq!(
2198 total_active_debug_lines, 1,
2199 "There should be exactly one active debug line across all editors after second stop"
2200 );
2201 })
2202 .unwrap();
2203
2204 // === New case: Move the debug pane (pane B) active item to a new pane C ===
2205 // This simulates a user dragging the tab with the active debug line to a new split.
2206 // The debugger should track that the debug line moved to pane C and use pane C
2207 // for subsequent debug stops.
2208
2209 // Split pane B to create pane C
2210 let pane_c = workspace
2211 .update(cx, |multi, window, cx| {
2212 multi.workspace().update(cx, |workspace, cx| {
2213 workspace.split_pane(pane_b.clone(), SplitDirection::Right, window, cx)
2214 })
2215 })
2216 .unwrap();
2217
2218 cx.run_until_parked();
2219
2220 // Move the active item (second.rs with debug line) from pane B to pane C
2221 workspace
2222 .update(cx, |_multi, window, cx| {
2223 move_active_item(&pane_b, &pane_c, true, false, window, cx);
2224 })
2225 .unwrap();
2226
2227 cx.run_until_parked();
2228
2229 // Verify pane C now has second.rs as active item
2230 workspace
2231 .read_with(cx, |_multi, cx| {
2232 let pane_c_active = pane_c.read(cx).active_item().unwrap();
2233 let pane_c_editor = pane_c_active.to_any_view().downcast::<Editor>().unwrap();
2234 let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap();
2235 assert_eq!(
2236 pane_c_path.path.file_name().unwrap(),
2237 "second.rs",
2238 "Pane C should have second.rs after moving it from pane B",
2239 );
2240 })
2241 .unwrap();
2242
2243 // Third breakpoint stop: back on main.rs line 2.
2244 // The debug line should appear in pane C because that's where the debug line
2245 // was moved to. The debugger should track pane moves.
2246 client.on_request::<StackTrace, _>(move |_, args| {
2247 assert_eq!(args.thread_id, 1);
2248
2249 Ok(dap::StackTraceResponse {
2250 stack_frames: vec![dap::StackFrame {
2251 id: 3,
2252 name: "frame 3".into(),
2253 source: Some(dap::Source {
2254 name: Some("main.rs".into()),
2255 path: Some(path!("/project/main.rs").into()),
2256 source_reference: None,
2257 presentation_hint: None,
2258 origin: None,
2259 sources: None,
2260 adapter_data: None,
2261 checksums: None,
2262 }),
2263 line: 2,
2264 column: 0,
2265 end_line: None,
2266 end_column: None,
2267 can_restart: None,
2268 instruction_pointer_reference: None,
2269 module_id: None,
2270 presentation_hint: None,
2271 }],
2272 total_frames: None,
2273 })
2274 });
2275
2276 client
2277 .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
2278 reason: dap::StoppedEventReason::Breakpoint,
2279 description: None,
2280 thread_id: Some(1),
2281 preserve_focus_hint: None,
2282 text: None,
2283 all_threads_stopped: None,
2284 hit_breakpoint_ids: None,
2285 }))
2286 .await;
2287
2288 cx.run_until_parked();
2289
2290 // Pane C should now have main.rs as the active tab with the debug line,
2291 // because pane C is where the debug line was moved to from pane B.
2292 workspace
2293 .read_with(cx, |_multi, cx| {
2294 let pane_c_active = pane_c.read(cx).active_item().unwrap();
2295 let pane_c_editor = pane_c_active.to_any_view().downcast::<Editor>().unwrap();
2296 let pane_c_path = pane_c_editor.read(cx).project_path(cx).unwrap();
2297 assert_eq!(
2298 pane_c_path.path.file_name().unwrap(),
2299 "main.rs",
2300 "Pane C should have switched to main.rs because it is now the persistent debug pane \
2301 (the debug line was moved here from pane B)",
2302 );
2303
2304 let active_debug_lines: Vec<_> = pane_c_editor
2305 .read(cx)
2306 .highlighted_rows::<ActiveDebugLine>()
2307 .collect();
2308
2309 assert_eq!(
2310 active_debug_lines.len(),
2311 1,
2312 "Pane C's main.rs editor should have the active debug line"
2313 );
2314 })
2315 .unwrap();
2316
2317 // There should still be exactly one active debug line across all editors
2318 workspace
2319 .read_with(cx, |_multi, cx| {
2320 let mut total_active_debug_lines = 0;
2321 for pane in [&pane_a, &pane_b, &pane_c] {
2322 for item in pane.read(cx).items() {
2323 if let Some(editor) = item.to_any_view().downcast::<Editor>().ok() {
2324 total_active_debug_lines += editor
2325 .read(cx)
2326 .highlighted_rows::<ActiveDebugLine>()
2327 .count();
2328 }
2329 }
2330 }
2331 assert_eq!(
2332 total_active_debug_lines, 1,
2333 "There should be exactly one active debug line across all editors after third stop"
2334 );
2335 })
2336 .unwrap();
2337
2338 // Clean up
2339 let shutdown_session = project.update(cx, |project, cx| {
2340 project.dap_store().update(cx, |dap_store, cx| {
2341 dap_store.shutdown_session(session.read(cx).session_id(), cx)
2342 })
2343 });
2344
2345 shutdown_session.await.unwrap();
2346}
2347
2348#[gpui::test]
2349async fn test_adapter_shutdown_with_child_sessions_on_app_quit(
2350 executor: BackgroundExecutor,
2351 cx: &mut TestAppContext,
2352) {
2353 init_test(cx);
2354
2355 let fs = FakeFs::new(executor.clone());
2356
2357 fs.insert_tree(
2358 path!("/project"),
2359 json!({
2360 "main.rs": "First line\nSecond line\nThird line\nFourth line",
2361 }),
2362 )
2363 .await;
2364
2365 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
2366 let workspace = init_test_workspace(&project, cx).await;
2367 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2368
2369 let parent_session = start_debug_session(&workspace, cx, |_| {}).unwrap();
2370 let parent_session_id = cx.read(|cx| parent_session.read(cx).session_id());
2371 let parent_client = parent_session.update(cx, |session, _| session.adapter_client().unwrap());
2372
2373 let disconnect_count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2374 let parent_disconnect_called = Arc::new(AtomicBool::new(false));
2375 let parent_disconnect_clone = parent_disconnect_called.clone();
2376 let disconnect_count_clone = disconnect_count.clone();
2377
2378 parent_client.on_request::<Disconnect, _>(move |_, _| {
2379 parent_disconnect_clone.store(true, Ordering::SeqCst);
2380 disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
2381
2382 for _ in 0..50 {
2383 if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
2384 break;
2385 }
2386 std::thread::sleep(std::time::Duration::from_millis(1));
2387 }
2388
2389 Ok(())
2390 });
2391
2392 parent_client
2393 .on_response::<StartDebugging, _>(move |_| {})
2394 .await;
2395 let _subscription = project::debugger::test::intercept_debug_sessions(cx, |_| {});
2396
2397 parent_client
2398 .fake_reverse_request::<StartDebugging>(StartDebuggingRequestArguments {
2399 configuration: json!({}),
2400 request: StartDebuggingRequestArgumentsRequest::Launch,
2401 })
2402 .await;
2403
2404 cx.run_until_parked();
2405
2406 let child_session = project.update(cx, |project, cx| {
2407 project
2408 .dap_store()
2409 .read(cx)
2410 .session_by_id(SessionId(1))
2411 .unwrap()
2412 });
2413 let child_session_id = cx.read(|cx| child_session.read(cx).session_id());
2414 let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap());
2415
2416 let child_disconnect_called = Arc::new(AtomicBool::new(false));
2417 let child_disconnect_clone = child_disconnect_called.clone();
2418 let disconnect_count_clone = disconnect_count.clone();
2419
2420 child_client.on_request::<Disconnect, _>(move |_, _| {
2421 child_disconnect_clone.store(true, Ordering::SeqCst);
2422 disconnect_count_clone.fetch_add(1, Ordering::SeqCst);
2423
2424 for _ in 0..50 {
2425 if disconnect_count_clone.load(Ordering::SeqCst) >= 2 {
2426 break;
2427 }
2428 std::thread::sleep(std::time::Duration::from_millis(1));
2429 }
2430
2431 Ok(())
2432 });
2433
2434 executor.run_until_parked();
2435
2436 project.update(cx, |project, cx| {
2437 let store = project.dap_store().read(cx);
2438 assert!(store.session_by_id(parent_session_id).is_some());
2439 assert!(store.session_by_id(child_session_id).is_some());
2440 });
2441
2442 cx.update(|_, cx| cx.defer(|cx| cx.shutdown()));
2443
2444 executor.run_until_parked();
2445
2446 let parent_disconnect_check = parent_disconnect_called.clone();
2447 let child_disconnect_check = child_disconnect_called.clone();
2448 let executor_clone = executor.clone();
2449 let both_disconnected = executor
2450 .spawn(async move {
2451 let parent_disconnect = parent_disconnect_check;
2452 let child_disconnect = child_disconnect_check;
2453
2454 // We only have 100ms to shutdown the app
2455 for _ in 0..100 {
2456 if parent_disconnect.load(Ordering::SeqCst)
2457 && child_disconnect.load(Ordering::SeqCst)
2458 {
2459 return true;
2460 }
2461
2462 executor_clone
2463 .timer(std::time::Duration::from_millis(1))
2464 .await;
2465 }
2466
2467 false
2468 })
2469 .await;
2470
2471 assert!(
2472 both_disconnected,
2473 "Both parent and child sessions should receive disconnect requests"
2474 );
2475
2476 assert!(
2477 parent_disconnect_called.load(Ordering::SeqCst),
2478 "Parent session should have received disconnect request"
2479 );
2480 assert!(
2481 child_disconnect_called.load(Ordering::SeqCst),
2482 "Child session should have received disconnect request"
2483 );
2484}
2485
2486#[gpui::test]
2487async fn test_restart_request_is_not_sent_more_than_once_until_response(
2488 executor: BackgroundExecutor,
2489 cx: &mut TestAppContext,
2490) {
2491 init_test(cx);
2492
2493 let fs = FakeFs::new(executor.clone());
2494
2495 fs.insert_tree(
2496 path!("/project"),
2497 json!({
2498 "main.rs": "First line\nSecond line\nThird line\nFourth line",
2499 }),
2500 )
2501 .await;
2502
2503 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
2504 let workspace = init_test_workspace(&project, cx).await;
2505 let cx = &mut VisualTestContext::from_window(*workspace, cx);
2506
2507 let session = start_debug_session(&workspace, cx, move |client| {
2508 client.on_request::<dap::requests::Initialize, _>(move |_, _| {
2509 Ok(dap::Capabilities {
2510 supports_restart_request: Some(true),
2511 ..Default::default()
2512 })
2513 });
2514 })
2515 .unwrap();
2516
2517 let client = session.update(cx, |session, _| session.adapter_client().unwrap());
2518
2519 let restart_count = Arc::new(AtomicUsize::new(0));
2520
2521 client.on_request::<dap::requests::Restart, _>({
2522 let restart_count = restart_count.clone();
2523 move |_, _| {
2524 restart_count.fetch_add(1, Ordering::SeqCst);
2525 Ok(())
2526 }
2527 });
2528
2529 // This works because the restart request sender is on the foreground thread
2530 // so it will start running after the gpui update stack is cleared
2531 session.update(cx, |session, cx| {
2532 session.restart(None, cx);
2533 session.restart(None, cx);
2534 session.restart(None, cx);
2535 });
2536
2537 cx.run_until_parked();
2538
2539 assert_eq!(
2540 restart_count.load(Ordering::SeqCst),
2541 1,
2542 "Only one restart request should be sent while a restart is in-flight"
2543 );
2544
2545 session.update(cx, |session, cx| {
2546 session.restart(None, cx);
2547 });
2548
2549 cx.run_until_parked();
2550
2551 assert_eq!(
2552 restart_count.load(Ordering::SeqCst),
2553 2,
2554 "A second restart should be allowed after the first one completes"
2555 );
2556}