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