Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:34:18.560Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

debugger_panel.rs

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