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