use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; use crate::{ DebugPanel, persistence::DebuggerPaneItem, session::running::variable_list::{ AddWatch, CollapseSelectedEntry, ExpandSelectedEntry, RemoveWatch, }, tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session}, }; use collections::HashMap; use dap::{ Scope, StackFrame, Variable, requests::{Evaluate, Initialize, Launch, Scopes, StackTrace, Variables}, }; use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; use menu::{SelectFirst, SelectNext, SelectPrevious}; use project::{FakeFs, Project}; use serde_json::json; use ui::SharedString; use unindent::Unindent as _; use util::path; /// This only tests fetching one scope and 2 variables for a single stackframe #[gpui::test] async fn test_basic_fetch_initial_scope_and_variables( executor: BackgroundExecutor, cx: &mut TestAppContext, ) { init_test(cx); let fs = FakeFs::new(executor.clone()); let test_file_content = r#" const variable1 = "Value 1"; const variable2 = "Value 2"; "# .unindent(); fs.insert_tree( path!("/project"), json!({ "src": { "test.js": test_file_content, } }), ) .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; workspace .update(cx, |workspace, window, cx| { workspace.focus_panel::(window, cx); }) .unwrap(); let cx = &mut VisualTestContext::from_window(*workspace, cx); let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); let client = session.update(cx, |session, _| session.adapter_client().unwrap()); client.on_request::(move |_, _| { Ok(dap::ThreadsResponse { threads: vec![dap::Thread { id: 1, name: "Thread 1".into(), }], }) }); let stack_frames = vec![StackFrame { id: 1, name: "Stack Frame 1".into(), source: Some(dap::Source { name: Some("test.js".into()), path: Some(path!("/project/src/test.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 1, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }]; client.on_request::({ let stack_frames = Arc::new(stack_frames.clone()); move |_, args| { assert_eq!(1, args.thread_id); Ok(dap::StackTraceResponse { stack_frames: (*stack_frames).clone(), total_frames: None, }) } }); let scopes = vec![Scope { name: "Scope 1".into(), presentation_hint: None, variables_reference: 2, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }]; client.on_request::({ let scopes = Arc::new(scopes.clone()); move |_, args| { assert_eq!(1, args.frame_id); Ok(dap::ScopesResponse { scopes: (*scopes).clone(), }) } }); let variables = vec![ Variable { name: "variable1".into(), value: "value 1".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "variable2".into(), value: "value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ]; client.on_request::({ let variables = Arc::new(variables.clone()); move |_, args| { assert_eq!(2, args.variables_reference); Ok(dap::VariablesResponse { variables: (*variables).clone(), }) } }); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, description: None, thread_id: Some(1), preserve_focus_hint: None, text: None, all_threads_stopped: None, hit_breakpoint_ids: None, })) .await; cx.run_until_parked(); let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { cx.focus_self(window); item.running_state().clone() }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { let (stack_frame_list, stack_frame_id) = running_state.stack_frame_list().update(cx, |list, _| { ( list.flatten_entries(true, true), list.opened_stack_frame_id(), ) }); assert_eq!(stack_frames, stack_frame_list); assert_eq!(Some(1), stack_frame_id); running_state .variable_list() .update(cx, |variable_list, _| { assert_eq!(scopes, variable_list.scopes()); assert_eq!( vec![variables[0].clone(), variables[1].clone(),], variable_list.variables() ); variable_list.assert_visual_entries(vec![ "v Scope 1", " > variable1", " > variable2", ]); }); }); } /// This tests fetching multiple scopes and variables for them with a single stackframe #[gpui::test] async fn test_fetch_variables_for_multiple_scopes( executor: BackgroundExecutor, cx: &mut TestAppContext, ) { init_test(cx); let fs = FakeFs::new(executor.clone()); let test_file_content = r#" const variable1 = { nested1: "Nested 1", nested2: "Nested 2", }; const variable2 = "Value 2"; const variable3 = "Value 3"; "# .unindent(); fs.insert_tree( path!("/project"), json!({ "src": { "test.js": test_file_content, } }), ) .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; workspace .update(cx, |workspace, window, cx| { workspace.focus_panel::(window, cx); }) .unwrap(); let cx = &mut VisualTestContext::from_window(*workspace, cx); let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); let client = session.update(cx, |session, _| session.adapter_client().unwrap()); client.on_request::(move |_, _| { Ok(dap::ThreadsResponse { threads: vec![dap::Thread { id: 1, name: "Thread 1".into(), }], }) }); client.on_request::(move |_, _| { Ok(dap::Capabilities { supports_step_back: Some(false), ..Default::default() }) }); client.on_request::(move |_, _| Ok(())); let stack_frames = vec![StackFrame { id: 1, name: "Stack Frame 1".into(), source: Some(dap::Source { name: Some("test.js".into()), path: Some(path!("/project/src/test.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 1, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }]; client.on_request::({ let stack_frames = Arc::new(stack_frames.clone()); move |_, args| { assert_eq!(1, args.thread_id); Ok(dap::StackTraceResponse { stack_frames: (*stack_frames).clone(), total_frames: None, }) } }); let scopes = vec![ Scope { name: "Scope 1".into(), presentation_hint: Some(dap::ScopePresentationHint::Locals), variables_reference: 2, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }, Scope { name: "Scope 2".into(), presentation_hint: None, variables_reference: 3, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }, ]; client.on_request::({ let scopes = Arc::new(scopes.clone()); move |_, args| { assert_eq!(1, args.frame_id); Ok(dap::ScopesResponse { scopes: (*scopes).clone(), }) } }); let mut variables = HashMap::default(); variables.insert( 2, vec![ Variable { name: "variable1".into(), value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "variable2".into(), value: "Value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ], ); variables.insert( 3, vec![Variable { name: "variable3".into(), value: "Value 3".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }], ); client.on_request::({ let variables = Arc::new(variables.clone()); move |_, args| { Ok(dap::VariablesResponse { variables: variables.get(&args.variables_reference).unwrap().clone(), }) } }); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, description: None, thread_id: Some(1), preserve_focus_hint: None, text: None, all_threads_stopped: None, hit_breakpoint_ids: None, })) .await; cx.run_until_parked(); let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { cx.focus_self(window); item.running_state().clone() }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { let (stack_frame_list, stack_frame_id) = running_state.stack_frame_list().update(cx, |list, _| { ( list.flatten_entries(true, true), list.opened_stack_frame_id(), ) }); assert_eq!(Some(1), stack_frame_id); assert_eq!(stack_frames, stack_frame_list); running_state .variable_list() .update(cx, |variable_list, _| { assert_eq!(2, variable_list.scopes().len()); assert_eq!(scopes, variable_list.scopes()); let variables_by_scope = variable_list.variables_per_scope(); // scope 1 assert_eq!( vec![ variables.get(&2).unwrap()[0].clone(), variables.get(&2).unwrap()[1].clone(), ], variables_by_scope[0].1 ); // scope 2 let empty_vec: Vec = vec![]; assert_eq!(empty_vec, variables_by_scope[1].1); variable_list.assert_visual_entries(vec![ "v Scope 1", " > variable1", " > variable2", "> Scope 2", ]); }); }); } // tests that toggling a variable will fetch its children and shows it #[gpui::test] async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(executor.clone()); let test_file_content = r#" const variable1 = { nested1: "Nested 1", nested2: "Nested 2", }; const variable2 = "Value 2"; const variable3 = "Value 3"; "# .unindent(); fs.insert_tree( path!("/project"), json!({ "src": { "test.js": test_file_content, } }), ) .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; workspace .update(cx, |workspace, window, cx| { workspace.focus_panel::(window, cx); }) .unwrap(); let cx = &mut VisualTestContext::from_window(*workspace, cx); let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); let client = session.update(cx, |session, _| session.adapter_client().unwrap()); client.on_request::(move |_, _| { Ok(dap::ThreadsResponse { threads: vec![dap::Thread { id: 1, name: "Thread 1".into(), }], }) }); client.on_request::(move |_, _| { Ok(dap::Capabilities { supports_step_back: Some(false), ..Default::default() }) }); client.on_request::(move |_, _| Ok(())); let stack_frames = vec![StackFrame { id: 1, name: "Stack Frame 1".into(), source: Some(dap::Source { name: Some("test.js".into()), path: Some(path!("/project/src/test.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 1, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }]; client.on_request::({ let stack_frames = Arc::new(stack_frames.clone()); move |_, args| { assert_eq!(1, args.thread_id); Ok(dap::StackTraceResponse { stack_frames: (*stack_frames).clone(), total_frames: None, }) } }); let scopes = vec![ Scope { name: "Scope 1".into(), presentation_hint: Some(dap::ScopePresentationHint::Locals), variables_reference: 2, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }, Scope { name: "Scope 2".into(), presentation_hint: None, variables_reference: 4, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }, ]; client.on_request::({ let scopes = Arc::new(scopes.clone()); move |_, args| { assert_eq!(1, args.frame_id); Ok(dap::ScopesResponse { scopes: (*scopes).clone(), }) } }); let scope1_variables = vec![ Variable { name: "variable1".into(), value: "{nested1: \"Nested 1\", nested2: \"Nested 2\"}".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 3, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "variable2".into(), value: "Value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ]; let nested_variables = vec![ Variable { name: "nested1".into(), value: "Nested 1".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "nested2".into(), value: "Nested 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ]; let scope2_variables = vec![Variable { name: "variable3".into(), value: "Value 3".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }]; client.on_request::({ let scope1_variables = Arc::new(scope1_variables.clone()); let nested_variables = Arc::new(nested_variables.clone()); let scope2_variables = Arc::new(scope2_variables.clone()); move |_, args| match args.variables_reference { 4 => Ok(dap::VariablesResponse { variables: (*scope2_variables).clone(), }), 3 => Ok(dap::VariablesResponse { variables: (*nested_variables).clone(), }), 2 => Ok(dap::VariablesResponse { variables: (*scope1_variables).clone(), }), id => unreachable!("unexpected variables reference {id}"), } }); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, description: None, thread_id: Some(1), preserve_focus_hint: None, text: None, all_threads_stopped: None, hit_breakpoint_ids: None, })) .await; cx.run_until_parked(); let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { cx.focus_self(window); let running = item.running_state().clone(); let variable_list = running.update(cx, |state, cx| { // have to do this because the variable list pane should be shown/active // for testing keyboard navigation state.activate_item(DebuggerPaneItem::Variables, window, cx); state.variable_list().clone() }); variable_list.update(cx, |_, cx| cx.focus_self(window)); running }); cx.dispatch_action(SelectFirst); cx.dispatch_action(SelectFirst); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { running_state .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1 <=== selected", " > variable1", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(SelectNext); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { running_state .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " > variable1 <=== selected", " > variable2", "> Scope 2", ]); }); }); // expand the nested variables of variable 1 cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { running_state .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1 <=== selected", " > nested1", " > nested2", " > variable2", "> Scope 2", ]); }); }); // select the first nested variable of variable 1 cx.dispatch_action(SelectNext); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1 <=== selected", " > nested2", " > variable2", "> Scope 2", ]); }); }); // select the second nested variable of variable 1 cx.dispatch_action(SelectNext); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2 <=== selected", " > variable2", "> Scope 2", ]); }); }); // select variable 2 of scope 1 cx.dispatch_action(SelectNext); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2", " > variable2 <=== selected", "> Scope 2", ]); }); }); // select scope 2 cx.dispatch_action(SelectNext); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2", " > variable2", "> Scope 2 <=== selected", ]); }); }); // expand the nested variables of scope 2 cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2", " > variable2", "v Scope 2 <=== selected", " > variable3", ]); }); }); // select variable 3 of scope 2 cx.dispatch_action(SelectNext); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2", " > variable2", "v Scope 2", " > variable3 <=== selected", ]); }); }); // select scope 2 cx.dispatch_action(SelectPrevious); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2", " > variable2", "v Scope 2 <=== selected", " > variable3", ]); }); }); // collapse variables of scope 2 cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2", " > variable2", "> Scope 2 <=== selected", ]); }); }); // select variable 2 of scope 1 cx.dispatch_action(SelectPrevious); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2", " > variable2 <=== selected", "> Scope 2", ]); }); }); // select nested2 of variable 1 cx.dispatch_action(SelectPrevious); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2 <=== selected", " > variable2", "> Scope 2", ]); }); }); // select nested1 of variable 1 cx.dispatch_action(SelectPrevious); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1 <=== selected", " > nested2", " > variable2", "> Scope 2", ]); }); }); // select variable 1 of scope 1 cx.dispatch_action(SelectPrevious); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1 <=== selected", " > nested1", " > nested2", " > variable2", "> Scope 2", ]); }); }); // collapse variables of variable 1 cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " > variable1 <=== selected", " > variable2", "> Scope 2", ]); }); }); // select scope 1 cx.dispatch_action(SelectPrevious); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { running_state .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1 <=== selected", " > variable1", " > variable2", "> Scope 2", ]); }); }); // collapse variables of scope 1 cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]); }); }); // select scope 2 backwards cx.dispatch_action(SelectPrevious); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec!["> Scope 1", "> Scope 2 <=== selected"]); }); }); // select scope 1 backwards cx.dispatch_action(SelectNext); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]); }); }); // test stepping through nested with ExpandSelectedEntry/CollapseSelectedEntry actions cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1 <=== selected", " > variable1", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " > variable1 <=== selected", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1 <=== selected", " > nested1", " > nested2", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1 <=== selected", " > nested2", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2 <=== selected", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(ExpandSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2", " > variable2 <=== selected", "> Scope 2", ]); }); }); cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1", " > nested2 <=== selected", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1", " > nested1 <=== selected", " > nested2", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " v variable1 <=== selected", " > nested1", " > nested2", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1", " > variable1 <=== selected", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec![ "v Scope 1 <=== selected", " > variable1", " > variable2", "> Scope 2", ]); }); }); cx.dispatch_action(CollapseSelectedEntry); cx.run_until_parked(); running_state.update(cx, |debug_panel_item, cx| { debug_panel_item .variable_list() .update(cx, |variable_list, _| { variable_list.assert_visual_entries(vec!["> Scope 1 <=== selected", "> Scope 2"]); }); }); } #[gpui::test] async fn test_variable_list_only_sends_requests_when_rendering( executor: BackgroundExecutor, cx: &mut TestAppContext, ) { init_test(cx); let fs = FakeFs::new(executor.clone()); let test_file_content = r#" import { SOME_VALUE } './module.js'; console.log(SOME_VALUE); "# .unindent(); let module_file_content = r#" export SOME_VALUE = 'some value'; "# .unindent(); fs.insert_tree( path!("/project"), json!({ "src": { "test.js": test_file_content, "module.js": module_file_content, } }), ) .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; let cx = &mut VisualTestContext::from_window(*workspace, cx); let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); let client = session.update(cx, |session, _| session.adapter_client().unwrap()); client.on_request::(move |_, _| { Ok(dap::ThreadsResponse { threads: vec![dap::Thread { id: 1, name: "Thread 1".into(), }], }) }); client.on_request::(move |_, _| { Ok(dap::Capabilities { supports_step_back: Some(false), ..Default::default() }) }); client.on_request::(move |_, _| Ok(())); let stack_frames = vec![ StackFrame { id: 1, name: "Stack Frame 1".into(), source: Some(dap::Source { name: Some("test.js".into()), path: Some(path!("/project/src/test.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 3, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }, StackFrame { id: 2, name: "Stack Frame 2".into(), source: Some(dap::Source { name: Some("module.js".into()), path: Some(path!("/project/src/module.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 1, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }, ]; client.on_request::({ let stack_frames = Arc::new(stack_frames.clone()); move |_, args| { assert_eq!(1, args.thread_id); Ok(dap::StackTraceResponse { stack_frames: (*stack_frames).clone(), total_frames: None, }) } }); let frame_1_scopes = vec![Scope { name: "Frame 1 Scope 1".into(), presentation_hint: None, variables_reference: 2, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }]; let made_scopes_request = Arc::new(AtomicBool::new(false)); client.on_request::({ let frame_1_scopes = Arc::new(frame_1_scopes.clone()); let made_scopes_request = made_scopes_request.clone(); move |_, args| { assert_eq!(1, args.frame_id); assert!( !made_scopes_request.load(Ordering::SeqCst), "We should be caching the scope request" ); made_scopes_request.store(true, Ordering::SeqCst); Ok(dap::ScopesResponse { scopes: (*frame_1_scopes).clone(), }) } }); let frame_1_variables = vec![ Variable { name: "variable1".into(), value: "value 1".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "variable2".into(), value: "value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ]; client.on_request::({ let frame_1_variables = Arc::new(frame_1_variables.clone()); move |_, args| { assert_eq!(2, args.variables_reference); Ok(dap::VariablesResponse { variables: (*frame_1_variables).clone(), }) } }); cx.run_until_parked(); let running_state = active_debug_session_panel(workspace, cx) .update_in(cx, |item, _, _| item.running_state().clone()); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, description: None, thread_id: Some(1), preserve_focus_hint: None, text: None, all_threads_stopped: None, hit_breakpoint_ids: None, })) .await; cx.run_until_parked(); running_state.update(cx, |running_state, cx| { let (stack_frame_list, stack_frame_id) = running_state.stack_frame_list().update(cx, |list, _| { ( list.flatten_entries(true, true), list.opened_stack_frame_id(), ) }); assert_eq!(Some(1), stack_frame_id); assert_eq!(stack_frames, stack_frame_list); let variable_list = running_state.variable_list().read(cx); assert_eq!(frame_1_variables, variable_list.variables()); assert!(made_scopes_request.load(Ordering::SeqCst)); }); } #[gpui::test] async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( executor: BackgroundExecutor, cx: &mut TestAppContext, ) { init_test(cx); let fs = FakeFs::new(executor.clone()); let test_file_content = r#" import { SOME_VALUE } './module.js'; console.log(SOME_VALUE); "# .unindent(); let module_file_content = r#" export SOME_VALUE = 'some value'; "# .unindent(); fs.insert_tree( path!("/project"), json!({ "src": { "test.js": test_file_content, "module.js": module_file_content, } }), ) .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; workspace .update(cx, |workspace, window, cx| { workspace.focus_panel::(window, cx); }) .unwrap(); let cx = &mut VisualTestContext::from_window(*workspace, cx); let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); let client = session.update(cx, |session, _| session.adapter_client().unwrap()); client.on_request::(move |_, _| { Ok(dap::ThreadsResponse { threads: vec![dap::Thread { id: 1, name: "Thread 1".into(), }], }) }); client.on_request::(move |_, _| { Ok(dap::Capabilities { supports_step_back: Some(false), ..Default::default() }) }); client.on_request::(move |_, _| Ok(())); let stack_frames = vec![ StackFrame { id: 1, name: "Stack Frame 1".into(), source: Some(dap::Source { name: Some("test.js".into()), path: Some(path!("/project/src/test.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 3, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }, StackFrame { id: 2, name: "Stack Frame 2".into(), source: Some(dap::Source { name: Some("module.js".into()), path: Some(path!("/project/src/module.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 1, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }, ]; client.on_request::({ let stack_frames = Arc::new(stack_frames.clone()); move |_, args| { assert_eq!(1, args.thread_id); Ok(dap::StackTraceResponse { stack_frames: (*stack_frames).clone(), total_frames: None, }) } }); let frame_1_scopes = vec![Scope { name: "Frame 1 Scope 1".into(), presentation_hint: None, variables_reference: 2, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }]; // add handlers for fetching the second stack frame's scopes and variables // after the user clicked the stack frame let frame_2_scopes = vec![Scope { name: "Frame 2 Scope 1".into(), presentation_hint: None, variables_reference: 3, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }]; let called_second_stack_frame = Arc::new(AtomicBool::new(false)); let called_first_stack_frame = Arc::new(AtomicBool::new(false)); client.on_request::({ let frame_1_scopes = Arc::new(frame_1_scopes.clone()); let frame_2_scopes = Arc::new(frame_2_scopes.clone()); let called_first_stack_frame = called_first_stack_frame.clone(); let called_second_stack_frame = called_second_stack_frame.clone(); move |_, args| match args.frame_id { 1 => { called_first_stack_frame.store(true, Ordering::SeqCst); Ok(dap::ScopesResponse { scopes: (*frame_1_scopes).clone(), }) } 2 => { called_second_stack_frame.store(true, Ordering::SeqCst); Ok(dap::ScopesResponse { scopes: (*frame_2_scopes).clone(), }) } _ => panic!("Made a scopes request with an invalid frame id"), } }); let frame_1_variables = vec![ Variable { name: "variable1".into(), value: "value 1".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "variable2".into(), value: "value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ]; let frame_2_variables = vec![ Variable { name: "variable3".into(), value: "old value 1".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "variable4".into(), value: "old value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ]; client.on_request::({ let frame_1_variables = Arc::new(frame_1_variables.clone()); move |_, args| { assert_eq!(2, args.variables_reference); Ok(dap::VariablesResponse { variables: (*frame_1_variables).clone(), }) } }); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, description: None, thread_id: Some(1), preserve_focus_hint: None, text: None, all_threads_stopped: None, hit_breakpoint_ids: None, })) .await; cx.run_until_parked(); let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { cx.focus_self(window); item.running_state().clone() }); running_state.update(cx, |running_state, cx| { let (stack_frame_list, stack_frame_id) = running_state.stack_frame_list().update(cx, |list, _| { ( list.flatten_entries(true, true), list.opened_stack_frame_id(), ) }); let variable_list = running_state.variable_list().read(cx); let variables = variable_list.variables(); assert_eq!(Some(1), stack_frame_id); assert_eq!( running_state .stack_frame_list() .read(cx) .opened_stack_frame_id(), Some(1) ); assert!( called_first_stack_frame.load(std::sync::atomic::Ordering::SeqCst), "Request scopes shouldn't be called before it's needed" ); assert!( !called_second_stack_frame.load(std::sync::atomic::Ordering::SeqCst), "Request scopes shouldn't be called before it's needed" ); assert_eq!(stack_frames, stack_frame_list); assert_eq!(frame_1_variables, variables); }); client.on_request::({ let frame_2_variables = Arc::new(frame_2_variables.clone()); move |_, args| { assert_eq!(3, args.variables_reference); Ok(dap::VariablesResponse { variables: (*frame_2_variables).clone(), }) } }); running_state .update_in(cx, |running_state, window, cx| { running_state .stack_frame_list() .update(cx, |stack_frame_list, cx| { stack_frame_list.go_to_stack_frame(stack_frames[1].id, window, cx) }) }) .await .unwrap(); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { let (stack_frame_list, stack_frame_id) = running_state.stack_frame_list().update(cx, |list, _| { ( list.flatten_entries(true, true), list.opened_stack_frame_id(), ) }); let variable_list = running_state.variable_list().read(cx); let variables = variable_list.variables(); assert_eq!(Some(2), stack_frame_id); assert!( called_second_stack_frame.load(std::sync::atomic::Ordering::SeqCst), "Request scopes shouldn't be called before it's needed" ); assert_eq!(stack_frames, stack_frame_list); assert_eq!(variables, frame_2_variables,); }); } #[gpui::test] async fn test_add_and_remove_watcher(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(executor.clone()); let test_file_content = r#" const variable1 = "Value 1"; const variable2 = "Value 2"; "# .unindent(); fs.insert_tree( path!("/project"), json!({ "src": { "test.js": test_file_content, } }), ) .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; workspace .update(cx, |workspace, window, cx| { workspace.focus_panel::(window, cx); }) .unwrap(); let cx = &mut VisualTestContext::from_window(*workspace, cx); let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); let client = session.update(cx, |session, _| session.adapter_client().unwrap()); client.on_request::(move |_, _| { Ok(dap::ThreadsResponse { threads: vec![dap::Thread { id: 1, name: "Thread 1".into(), }], }) }); let stack_frames = vec![StackFrame { id: 1, name: "Stack Frame 1".into(), source: Some(dap::Source { name: Some("test.js".into()), path: Some(path!("/project/src/test.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 1, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }]; client.on_request::({ let stack_frames = Arc::new(stack_frames.clone()); move |_, args| { assert_eq!(1, args.thread_id); Ok(dap::StackTraceResponse { stack_frames: (*stack_frames).clone(), total_frames: None, }) } }); let scopes = vec![Scope { name: "Scope 1".into(), presentation_hint: None, variables_reference: 2, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }]; client.on_request::({ let scopes = Arc::new(scopes.clone()); move |_, args| { assert_eq!(1, args.frame_id); Ok(dap::ScopesResponse { scopes: (*scopes).clone(), }) } }); let variables = vec![ Variable { name: "variable1".into(), value: "value 1".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "variable2".into(), value: "value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ]; client.on_request::({ let variables = Arc::new(variables.clone()); move |_, args| { assert_eq!(2, args.variables_reference); Ok(dap::VariablesResponse { variables: (*variables).clone(), }) } }); client.on_request::({ move |_, args| { assert_eq!("variable1", args.expression); Ok(dap::EvaluateResponse { result: "value1".to_owned(), type_: None, presentation_hint: None, variables_reference: 2, named_variables: None, indexed_variables: None, memory_reference: None, value_location_reference: None, }) } }); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, description: None, thread_id: Some(1), preserve_focus_hint: None, text: None, all_threads_stopped: None, hit_breakpoint_ids: None, })) .await; cx.run_until_parked(); let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { cx.focus_self(window); let running = item.running_state().clone(); let variable_list = running.update(cx, |state, cx| { // have to do this because the variable list pane should be shown/active // for testing the variable list state.activate_item(DebuggerPaneItem::Variables, window, cx); state.variable_list().clone() }); variable_list.update(cx, |_, cx| cx.focus_self(window)); running }); cx.run_until_parked(); // select variable 1 from first scope running_state.update(cx, |running_state, cx| { running_state.variable_list().update(cx, |_, cx| { cx.dispatch_action(&SelectFirst); cx.dispatch_action(&SelectNext); }); }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { running_state.variable_list().update(cx, |_, cx| { cx.dispatch_action(&AddWatch); }); }); cx.run_until_parked(); // assert watcher for variable1 was added running_state.update(cx, |running_state, cx| { running_state.variable_list().update(cx, |list, _| { list.assert_visual_entries(vec![ "> variable1", "v Scope 1", " > variable1 <=== selected", " > variable2", ]); }); }); session.update(cx, |session, _| { let watcher = session .watchers() .get(&SharedString::from("variable1")) .unwrap(); assert_eq!("value1", watcher.value.to_string()); assert_eq!("variable1", watcher.expression.to_string()); assert_eq!(2, watcher.variables_reference); }); // select added watcher for variable1 running_state.update(cx, |running_state, cx| { running_state.variable_list().update(cx, |_, cx| { cx.dispatch_action(&SelectFirst); }); }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { running_state.variable_list().update(cx, |_, cx| { cx.dispatch_action(&RemoveWatch); }); }); cx.run_until_parked(); // assert watcher for variable1 was removed running_state.update(cx, |running_state, cx| { running_state.variable_list().update(cx, |list, _| { list.assert_visual_entries(vec!["v Scope 1", " > variable1", " > variable2"]); }); }); } #[gpui::test] async fn test_refresh_watchers(executor: BackgroundExecutor, cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(executor.clone()); let test_file_content = r#" const variable1 = "Value 1"; const variable2 = "Value 2"; "# .unindent(); fs.insert_tree( path!("/project"), json!({ "src": { "test.js": test_file_content, } }), ) .await; let project = Project::test(fs, [path!("/project").as_ref()], cx).await; let workspace = init_test_workspace(&project, cx).await; workspace .update(cx, |workspace, window, cx| { workspace.focus_panel::(window, cx); }) .unwrap(); let cx = &mut VisualTestContext::from_window(*workspace, cx); let session = start_debug_session(&workspace, cx, |_| {}).unwrap(); let client = session.update(cx, |session, _| session.adapter_client().unwrap()); client.on_request::(move |_, _| { Ok(dap::ThreadsResponse { threads: vec![dap::Thread { id: 1, name: "Thread 1".into(), }], }) }); let stack_frames = vec![StackFrame { id: 1, name: "Stack Frame 1".into(), source: Some(dap::Source { name: Some("test.js".into()), path: Some(path!("/project/src/test.js").into()), source_reference: None, presentation_hint: None, origin: None, sources: None, adapter_data: None, checksums: None, }), line: 1, column: 1, end_line: None, end_column: None, can_restart: None, instruction_pointer_reference: None, module_id: None, presentation_hint: None, }]; client.on_request::({ let stack_frames = Arc::new(stack_frames.clone()); move |_, args| { assert_eq!(1, args.thread_id); Ok(dap::StackTraceResponse { stack_frames: (*stack_frames).clone(), total_frames: None, }) } }); let scopes = vec![Scope { name: "Scope 1".into(), presentation_hint: None, variables_reference: 2, named_variables: None, indexed_variables: None, expensive: false, source: None, line: None, column: None, end_line: None, end_column: None, }]; client.on_request::({ let scopes = Arc::new(scopes.clone()); move |_, args| { assert_eq!(1, args.frame_id); Ok(dap::ScopesResponse { scopes: (*scopes).clone(), }) } }); let variables = vec![ Variable { name: "variable1".into(), value: "value 1".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, Variable { name: "variable2".into(), value: "value 2".into(), type_: None, presentation_hint: None, evaluate_name: None, variables_reference: 0, named_variables: None, indexed_variables: None, memory_reference: None, declaration_location_reference: None, value_location_reference: None, }, ]; client.on_request::({ let variables = Arc::new(variables.clone()); move |_, args| { assert_eq!(2, args.variables_reference); Ok(dap::VariablesResponse { variables: (*variables).clone(), }) } }); client.on_request::({ move |_, args| { assert_eq!("variable1", args.expression); Ok(dap::EvaluateResponse { result: "value1".to_owned(), type_: None, presentation_hint: None, variables_reference: 2, named_variables: None, indexed_variables: None, memory_reference: None, value_location_reference: None, }) } }); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, description: None, thread_id: Some(1), preserve_focus_hint: None, text: None, all_threads_stopped: None, hit_breakpoint_ids: None, })) .await; cx.run_until_parked(); let running_state = active_debug_session_panel(workspace, cx).update_in(cx, |item, window, cx| { cx.focus_self(window); let running = item.running_state().clone(); let variable_list = running.update(cx, |state, cx| { // have to do this because the variable list pane should be shown/active // for testing the variable list state.activate_item(DebuggerPaneItem::Variables, window, cx); state.variable_list().clone() }); variable_list.update(cx, |_, cx| cx.focus_self(window)); running }); cx.run_until_parked(); // select variable 1 from first scope running_state.update(cx, |running_state, cx| { running_state.variable_list().update(cx, |_, cx| { cx.dispatch_action(&SelectFirst); cx.dispatch_action(&SelectNext); }); }); cx.run_until_parked(); running_state.update(cx, |running_state, cx| { running_state.variable_list().update(cx, |_, cx| { cx.dispatch_action(&AddWatch); }); }); cx.run_until_parked(); session.update(cx, |session, _| { let watcher = session .watchers() .get(&SharedString::from("variable1")) .unwrap(); assert_eq!("value1", watcher.value.to_string()); assert_eq!("variable1", watcher.expression.to_string()); assert_eq!(2, watcher.variables_reference); }); client.on_request::({ move |_, args| { assert_eq!("variable1", args.expression); Ok(dap::EvaluateResponse { result: "value updated".to_owned(), type_: None, presentation_hint: None, variables_reference: 3, named_variables: None, indexed_variables: None, memory_reference: None, value_location_reference: None, }) } }); client .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { reason: dap::StoppedEventReason::Pause, description: None, thread_id: Some(1), preserve_focus_hint: None, text: None, all_threads_stopped: None, hit_breakpoint_ids: None, })) .await; cx.run_until_parked(); session.update(cx, |session, _| { let watcher = session .watchers() .get(&SharedString::from("variable1")) .unwrap(); assert_eq!("value updated", watcher.value.to_string()); assert_eq!("variable1", watcher.expression.to_string()); assert_eq!(3, watcher.variables_reference); }); }