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