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