stack_frame_list.rs

  1use crate::{
  2    debugger_panel::DebugPanel,
  3    session::running::stack_frame_list::StackFrameEntry,
  4    tests::{active_debug_session_panel, init_test, init_test_workspace},
  5};
  6use dap::{
  7    StackFrame,
  8    requests::{Scopes, StackTrace, Threads},
  9};
 10use editor::{Editor, ToPoint as _};
 11use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 12use project::{FakeFs, Project, debugger};
 13use serde_json::json;
 14use std::sync::Arc;
 15use unindent::Unindent as _;
 16use util::path;
 17
 18#[gpui::test]
 19async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
 20    executor: BackgroundExecutor,
 21    cx: &mut TestAppContext,
 22) {
 23    init_test(cx);
 24
 25    let fs = FakeFs::new(executor.clone());
 26
 27    let test_file_content = r#"
 28        import { SOME_VALUE } './module.js';
 29
 30        console.log(SOME_VALUE);
 31    "#
 32    .unindent();
 33
 34    let module_file_content = r#"
 35        export SOME_VALUE = 'some value';
 36    "#
 37    .unindent();
 38
 39    fs.insert_tree(
 40        path!("/project"),
 41        json!({
 42           "src": {
 43               "test.js": test_file_content,
 44               "module.js": module_file_content,
 45           }
 46        }),
 47    )
 48    .await;
 49
 50    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
 51    let workspace = init_test_workspace(&project, cx).await;
 52    let cx = &mut VisualTestContext::from_window(*workspace, cx);
 53    let session = debugger::test::start_debug_session(&project, cx, |_| {})
 54        .await
 55        .unwrap();
 56    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
 57    client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
 58
 59    client.on_request::<Threads, _>(move |_, _| {
 60        Ok(dap::ThreadsResponse {
 61            threads: vec![dap::Thread {
 62                id: 1,
 63                name: "Thread 1".into(),
 64            }],
 65        })
 66    });
 67
 68    let stack_frames = vec![
 69        StackFrame {
 70            id: 1,
 71            name: "Stack Frame 1".into(),
 72            source: Some(dap::Source {
 73                name: Some("test.js".into()),
 74                path: Some(path!("/project/src/test.js").into()),
 75                source_reference: None,
 76                presentation_hint: None,
 77                origin: None,
 78                sources: None,
 79                adapter_data: None,
 80                checksums: None,
 81            }),
 82            line: 3,
 83            column: 1,
 84            end_line: None,
 85            end_column: None,
 86            can_restart: None,
 87            instruction_pointer_reference: None,
 88            module_id: None,
 89            presentation_hint: None,
 90        },
 91        StackFrame {
 92            id: 2,
 93            name: "Stack Frame 2".into(),
 94            source: Some(dap::Source {
 95                name: Some("module.js".into()),
 96                path: Some(path!("/project/src/module.js").into()),
 97                source_reference: None,
 98                presentation_hint: None,
 99                origin: None,
100                sources: None,
101                adapter_data: None,
102                checksums: None,
103            }),
104            line: 1,
105            column: 1,
106            end_line: None,
107            end_column: None,
108            can_restart: None,
109            instruction_pointer_reference: None,
110            module_id: None,
111            presentation_hint: None,
112        },
113    ];
114
115    client.on_request::<StackTrace, _>({
116        let stack_frames = Arc::new(stack_frames.clone());
117        move |_, args| {
118            assert_eq!(1, args.thread_id);
119
120            Ok(dap::StackTraceResponse {
121                stack_frames: (*stack_frames).clone(),
122                total_frames: None,
123            })
124        }
125    });
126
127    client
128        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
129            reason: dap::StoppedEventReason::Pause,
130            description: None,
131            thread_id: Some(1),
132            preserve_focus_hint: None,
133            text: None,
134            all_threads_stopped: None,
135            hit_breakpoint_ids: None,
136        }))
137        .await;
138
139    cx.run_until_parked();
140
141    // trigger to load threads
142    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
143        session
144            .mode()
145            .as_running()
146            .unwrap()
147            .update(cx, |running_state, cx| {
148                running_state
149                    .session()
150                    .update(cx, |session, cx| session.threads(cx));
151            });
152    });
153
154    cx.run_until_parked();
155
156    // select first thread
157    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
158        session
159            .mode()
160            .as_running()
161            .unwrap()
162            .update(cx, |running_state, cx| {
163                running_state.select_current_thread(
164                    &running_state
165                        .session()
166                        .update(cx, |session, cx| session.threads(cx)),
167                    cx,
168                );
169            });
170    });
171
172    cx.run_until_parked();
173
174    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
175        let stack_frame_list = session
176            .mode()
177            .as_running()
178            .unwrap()
179            .update(cx, |state, _| state.stack_frame_list().clone());
180
181        stack_frame_list.update(cx, |stack_frame_list, cx| {
182            assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
183            assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
184        });
185    });
186
187    let shutdown_session = project.update(cx, |project, cx| {
188        project.dap_store().update(cx, |dap_store, cx| {
189            dap_store.shutdown_session(session.read(cx).session_id(), cx)
190        })
191    });
192
193    shutdown_session.await.unwrap();
194}
195
196#[gpui::test]
197async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppContext) {
198    init_test(cx);
199
200    let fs = FakeFs::new(executor.clone());
201
202    let test_file_content = r#"
203        import { SOME_VALUE } './module.js';
204
205        console.log(SOME_VALUE);
206    "#
207    .unindent();
208
209    let module_file_content = r#"
210        export SOME_VALUE = 'some value';
211    "#
212    .unindent();
213
214    fs.insert_tree(
215        path!("/project"),
216        json!({
217           "src": {
218               "test.js": test_file_content,
219               "module.js": module_file_content,
220           }
221        }),
222    )
223    .await;
224
225    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
226    let workspace = init_test_workspace(&project, cx).await;
227    let _ = workspace.update(cx, |workspace, window, cx| {
228        workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx);
229    });
230
231    let cx = &mut VisualTestContext::from_window(*workspace, cx);
232    let session = debugger::test::start_debug_session(&project, cx, |_| {})
233        .await
234        .unwrap();
235    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
236
237    client.on_request::<Threads, _>(move |_, _| {
238        Ok(dap::ThreadsResponse {
239            threads: vec![dap::Thread {
240                id: 1,
241                name: "Thread 1".into(),
242            }],
243        })
244    });
245
246    client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
247
248    let stack_frames = vec![
249        StackFrame {
250            id: 1,
251            name: "Stack Frame 1".into(),
252            source: Some(dap::Source {
253                name: Some("test.js".into()),
254                path: Some(path!("/project/src/test.js").into()),
255                source_reference: None,
256                presentation_hint: None,
257                origin: None,
258                sources: None,
259                adapter_data: None,
260                checksums: None,
261            }),
262            line: 3,
263            column: 1,
264            end_line: None,
265            end_column: None,
266            can_restart: None,
267            instruction_pointer_reference: None,
268            module_id: None,
269            presentation_hint: None,
270        },
271        StackFrame {
272            id: 2,
273            name: "Stack Frame 2".into(),
274            source: Some(dap::Source {
275                name: Some("module.js".into()),
276                path: Some(path!("/project/src/module.js").into()),
277                source_reference: None,
278                presentation_hint: None,
279                origin: None,
280                sources: None,
281                adapter_data: None,
282                checksums: None,
283            }),
284            line: 1,
285            column: 1,
286            end_line: None,
287            end_column: None,
288            can_restart: None,
289            instruction_pointer_reference: None,
290            module_id: None,
291            presentation_hint: None,
292        },
293    ];
294
295    client.on_request::<StackTrace, _>({
296        let stack_frames = Arc::new(stack_frames.clone());
297        move |_, args| {
298            assert_eq!(1, args.thread_id);
299
300            Ok(dap::StackTraceResponse {
301                stack_frames: (*stack_frames).clone(),
302                total_frames: None,
303            })
304        }
305    });
306
307    client
308        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
309            reason: dap::StoppedEventReason::Pause,
310            description: None,
311            thread_id: Some(1),
312            preserve_focus_hint: None,
313            text: None,
314            all_threads_stopped: None,
315            hit_breakpoint_ids: None,
316        }))
317        .await;
318
319    cx.run_until_parked();
320
321    // trigger threads to load
322    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
323        session
324            .mode()
325            .as_running()
326            .unwrap()
327            .update(cx, |running_state, cx| {
328                running_state
329                    .session()
330                    .update(cx, |session, cx| session.threads(cx));
331            });
332    });
333
334    cx.run_until_parked();
335
336    // select first thread
337    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
338        session
339            .mode()
340            .as_running()
341            .unwrap()
342            .update(cx, |running_state, cx| {
343                running_state.select_current_thread(
344                    &running_state
345                        .session()
346                        .update(cx, |session, cx| session.threads(cx)),
347                    cx,
348                );
349            });
350    });
351
352    cx.run_until_parked();
353
354    workspace
355        .update(cx, |workspace, window, cx| {
356            let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
357            assert_eq!(1, editors.len());
358
359            let project_path = editors[0]
360                .update(cx, |editor, cx| editor.project_path(cx))
361                .unwrap();
362            let expected = if cfg!(target_os = "windows") {
363                "src\\test.js"
364            } else {
365                "src/test.js"
366            };
367            assert_eq!(expected, project_path.path.to_string_lossy());
368            assert_eq!(test_file_content, editors[0].read(cx).text(cx));
369            assert_eq!(
370                vec![2..3],
371                editors[0].update(cx, |editor, cx| {
372                    let snapshot = editor.snapshot(window, cx);
373
374                    editor
375                        .highlighted_rows::<editor::DebugCurrentRowHighlight>()
376                        .map(|(range, _)| {
377                            let start = range.start.to_point(&snapshot.buffer_snapshot);
378                            let end = range.end.to_point(&snapshot.buffer_snapshot);
379                            start.row..end.row
380                        })
381                        .collect::<Vec<_>>()
382                })
383            );
384        })
385        .unwrap();
386
387    let stack_frame_list = workspace
388        .update(cx, |workspace, _window, cx| {
389            let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
390            let active_debug_panel_item = debug_panel
391                .update(cx, |this, _| this.active_session())
392                .unwrap();
393
394            active_debug_panel_item
395                .read(cx)
396                .mode()
397                .as_running()
398                .unwrap()
399                .read(cx)
400                .stack_frame_list()
401                .clone()
402        })
403        .unwrap();
404
405    stack_frame_list.update(cx, |stack_frame_list, cx| {
406        assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
407        assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
408    });
409
410    // select second stack frame
411    stack_frame_list
412        .update_in(cx, |stack_frame_list, window, cx| {
413            stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
414        })
415        .await
416        .unwrap();
417
418    cx.run_until_parked();
419
420    stack_frame_list.update(cx, |stack_frame_list, cx| {
421        assert_eq!(Some(2), stack_frame_list.selected_stack_frame_id());
422        assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
423    });
424
425    let _ = workspace.update(cx, |workspace, window, cx| {
426        let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
427        assert_eq!(1, editors.len());
428
429        let project_path = editors[0]
430            .update(cx, |editor, cx| editor.project_path(cx))
431            .unwrap();
432        let expected = if cfg!(target_os = "windows") {
433            "src\\module.js"
434        } else {
435            "src/module.js"
436        };
437        assert_eq!(expected, project_path.path.to_string_lossy());
438        assert_eq!(module_file_content, editors[0].read(cx).text(cx));
439        assert_eq!(
440            vec![0..1],
441            editors[0].update(cx, |editor, cx| {
442                let snapshot = editor.snapshot(window, cx);
443
444                editor
445                    .highlighted_rows::<editor::DebugCurrentRowHighlight>()
446                    .map(|(range, _)| {
447                        let start = range.start.to_point(&snapshot.buffer_snapshot);
448                        let end = range.end.to_point(&snapshot.buffer_snapshot);
449                        start.row..end.row
450                    })
451                    .collect::<Vec<_>>()
452            })
453        );
454    });
455
456    let shutdown_session = project.update(cx, |project, cx| {
457        project.dap_store().update(cx, |dap_store, cx| {
458            dap_store.shutdown_session(session.read(cx).session_id(), cx)
459        })
460    });
461
462    shutdown_session.await.unwrap();
463}
464
465#[gpui::test]
466async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppContext) {
467    init_test(cx);
468
469    let fs = FakeFs::new(executor.clone());
470
471    let test_file_content = r#"
472        import { SOME_VALUE } './module.js';
473
474        console.log(SOME_VALUE);
475    "#
476    .unindent();
477
478    let module_file_content = r#"
479        export SOME_VALUE = 'some value';
480    "#
481    .unindent();
482
483    fs.insert_tree(
484        path!("/project"),
485        json!({
486           "src": {
487               "test.js": test_file_content,
488               "module.js": module_file_content,
489           }
490        }),
491    )
492    .await;
493
494    let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
495    let workspace = init_test_workspace(&project, cx).await;
496    let cx = &mut VisualTestContext::from_window(*workspace, cx);
497
498    let session = debugger::test::start_debug_session(&project, cx, |_| {})
499        .await
500        .unwrap();
501    let client = session.update(cx, |session, _| session.adapter_client().unwrap());
502
503    client.on_request::<Threads, _>(move |_, _| {
504        Ok(dap::ThreadsResponse {
505            threads: vec![dap::Thread {
506                id: 1,
507                name: "Thread 1".into(),
508            }],
509        })
510    });
511
512    client.on_request::<Scopes, _>(move |_, _| Ok(dap::ScopesResponse { scopes: vec![] }));
513
514    let stack_frames = vec![
515        StackFrame {
516            id: 1,
517            name: "Stack Frame 1".into(),
518            source: Some(dap::Source {
519                name: Some("test.js".into()),
520                path: Some(path!("/project/src/test.js").into()),
521                source_reference: None,
522                presentation_hint: None,
523                origin: None,
524                sources: None,
525                adapter_data: None,
526                checksums: None,
527            }),
528            line: 3,
529            column: 1,
530            end_line: None,
531            end_column: None,
532            can_restart: None,
533            instruction_pointer_reference: None,
534            module_id: None,
535            presentation_hint: None,
536        },
537        StackFrame {
538            id: 2,
539            name: "Stack Frame 2".into(),
540            source: Some(dap::Source {
541                name: Some("module.js".into()),
542                path: Some(path!("/project/src/module.js").into()),
543                source_reference: None,
544                presentation_hint: None,
545                origin: Some("ignored".into()),
546                sources: None,
547                adapter_data: None,
548                checksums: None,
549            }),
550            line: 1,
551            column: 1,
552            end_line: None,
553            end_column: None,
554            can_restart: None,
555            instruction_pointer_reference: None,
556            module_id: None,
557            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
558        },
559        StackFrame {
560            id: 3,
561            name: "Stack Frame 3".into(),
562            source: Some(dap::Source {
563                name: Some("module.js".into()),
564                path: Some(path!("/project/src/module.js").into()),
565                source_reference: None,
566                presentation_hint: None,
567                origin: Some("ignored".into()),
568                sources: None,
569                adapter_data: None,
570                checksums: None,
571            }),
572            line: 1,
573            column: 1,
574            end_line: None,
575            end_column: None,
576            can_restart: None,
577            instruction_pointer_reference: None,
578            module_id: None,
579            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
580        },
581        StackFrame {
582            id: 4,
583            name: "Stack Frame 4".into(),
584            source: Some(dap::Source {
585                name: Some("module.js".into()),
586                path: Some(path!("/project/src/module.js").into()),
587                source_reference: None,
588                presentation_hint: None,
589                origin: None,
590                sources: None,
591                adapter_data: None,
592                checksums: None,
593            }),
594            line: 1,
595            column: 1,
596            end_line: None,
597            end_column: None,
598            can_restart: None,
599            instruction_pointer_reference: None,
600            module_id: None,
601            presentation_hint: None,
602        },
603        StackFrame {
604            id: 5,
605            name: "Stack Frame 5".into(),
606            source: Some(dap::Source {
607                name: Some("module.js".into()),
608                path: Some(path!("/project/src/module.js").into()),
609                source_reference: None,
610                presentation_hint: None,
611                origin: None,
612                sources: None,
613                adapter_data: None,
614                checksums: None,
615            }),
616            line: 1,
617            column: 1,
618            end_line: None,
619            end_column: None,
620            can_restart: None,
621            instruction_pointer_reference: None,
622            module_id: None,
623            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
624        },
625        StackFrame {
626            id: 6,
627            name: "Stack Frame 6".into(),
628            source: Some(dap::Source {
629                name: Some("module.js".into()),
630                path: Some(path!("/project/src/module.js").into()),
631                source_reference: None,
632                presentation_hint: None,
633                origin: None,
634                sources: None,
635                adapter_data: None,
636                checksums: None,
637            }),
638            line: 1,
639            column: 1,
640            end_line: None,
641            end_column: None,
642            can_restart: None,
643            instruction_pointer_reference: None,
644            module_id: None,
645            presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
646        },
647        StackFrame {
648            id: 7,
649            name: "Stack Frame 7".into(),
650            source: Some(dap::Source {
651                name: Some("module.js".into()),
652                path: Some(path!("/project/src/module.js").into()),
653                source_reference: None,
654                presentation_hint: None,
655                origin: None,
656                sources: None,
657                adapter_data: None,
658                checksums: None,
659            }),
660            line: 1,
661            column: 1,
662            end_line: None,
663            end_column: None,
664            can_restart: None,
665            instruction_pointer_reference: None,
666            module_id: None,
667            presentation_hint: None,
668        },
669    ];
670
671    client.on_request::<StackTrace, _>({
672        let stack_frames = Arc::new(stack_frames.clone());
673        move |_, args| {
674            assert_eq!(1, args.thread_id);
675
676            Ok(dap::StackTraceResponse {
677                stack_frames: (*stack_frames).clone(),
678                total_frames: None,
679            })
680        }
681    });
682
683    client
684        .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent {
685            reason: dap::StoppedEventReason::Pause,
686            description: None,
687            thread_id: Some(1),
688            preserve_focus_hint: None,
689            text: None,
690            all_threads_stopped: None,
691            hit_breakpoint_ids: None,
692        }))
693        .await;
694
695    cx.run_until_parked();
696
697    // trigger threads to load
698    active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
699        session
700            .mode()
701            .as_running()
702            .unwrap()
703            .update(cx, |running_state, cx| {
704                running_state
705                    .session()
706                    .update(cx, |session, cx| session.threads(cx));
707            });
708    });
709
710    cx.run_until_parked();
711
712    // select first thread
713    active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
714        session
715            .mode()
716            .as_running()
717            .unwrap()
718            .update(cx, |running_state, cx| {
719                running_state.select_current_thread(
720                    &running_state
721                        .session()
722                        .update(cx, |session, cx| session.threads(cx)),
723                    cx,
724                );
725            });
726    });
727
728    cx.run_until_parked();
729
730    // trigger stack frames to loaded
731    active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
732        let stack_frame_list = debug_panel_item
733            .mode()
734            .as_running()
735            .unwrap()
736            .update(cx, |state, _| state.stack_frame_list().clone());
737
738        stack_frame_list.update(cx, |stack_frame_list, cx| {
739            stack_frame_list.dap_stack_frames(cx);
740        });
741    });
742
743    cx.run_until_parked();
744
745    active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
746        let stack_frame_list = debug_panel_item
747            .mode()
748            .as_running()
749            .unwrap()
750            .update(cx, |state, _| state.stack_frame_list().clone());
751
752        stack_frame_list.update(cx, |stack_frame_list, cx| {
753            stack_frame_list.build_entries(true, window, cx);
754
755            assert_eq!(
756                &vec![
757                    StackFrameEntry::Normal(stack_frames[0].clone()),
758                    StackFrameEntry::Collapsed(vec![
759                        stack_frames[1].clone(),
760                        stack_frames[2].clone()
761                    ]),
762                    StackFrameEntry::Normal(stack_frames[3].clone()),
763                    StackFrameEntry::Collapsed(vec![
764                        stack_frames[4].clone(),
765                        stack_frames[5].clone()
766                    ]),
767                    StackFrameEntry::Normal(stack_frames[6].clone()),
768                ],
769                stack_frame_list.entries()
770            );
771
772            stack_frame_list.expand_collapsed_entry(
773                1,
774                &vec![stack_frames[1].clone(), stack_frames[2].clone()],
775                cx,
776            );
777
778            assert_eq!(
779                &vec![
780                    StackFrameEntry::Normal(stack_frames[0].clone()),
781                    StackFrameEntry::Normal(stack_frames[1].clone()),
782                    StackFrameEntry::Normal(stack_frames[2].clone()),
783                    StackFrameEntry::Normal(stack_frames[3].clone()),
784                    StackFrameEntry::Collapsed(vec![
785                        stack_frames[4].clone(),
786                        stack_frames[5].clone()
787                    ]),
788                    StackFrameEntry::Normal(stack_frames[6].clone()),
789                ],
790                stack_frame_list.entries()
791            );
792
793            stack_frame_list.expand_collapsed_entry(
794                4,
795                &vec![stack_frames[4].clone(), stack_frames[5].clone()],
796                cx,
797            );
798
799            assert_eq!(
800                &vec![
801                    StackFrameEntry::Normal(stack_frames[0].clone()),
802                    StackFrameEntry::Normal(stack_frames[1].clone()),
803                    StackFrameEntry::Normal(stack_frames[2].clone()),
804                    StackFrameEntry::Normal(stack_frames[3].clone()),
805                    StackFrameEntry::Normal(stack_frames[4].clone()),
806                    StackFrameEntry::Normal(stack_frames[5].clone()),
807                    StackFrameEntry::Normal(stack_frames[6].clone()),
808                ],
809                stack_frame_list.entries()
810            );
811        });
812    });
813
814    let shutdown_session = project.update(cx, |project, cx| {
815        project.dap_store().update(cx, |dap_store, cx| {
816            dap_store.shutdown_session(session.read(cx).session_id(), cx)
817        })
818    });
819
820    shutdown_session.await.unwrap();
821}