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