stack_frame_list.rs

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