Debugger implementation (#13433)

###  DISCLAIMER

> As of 6th March 2025, debugger is still in development. We plan to
merge it behind a staff-only feature flag for staff use only, followed
by non-public release and then finally a public one (akin to how Git
panel release was handled). This is done to ensure the best experience
when it gets released.

### END OF DISCLAIMER 

**The current state of the debugger implementation:**


https://github.com/user-attachments/assets/c4deff07-80dd-4dc6-ad2e-0c252a478fe9


https://github.com/user-attachments/assets/e1ed2345-b750-4bb6-9c97-50961b76904f

----

All the todo's are in the following channel, so it's easier to work on
this together:
https://zed.dev/channel/zed-debugger-11370

If you are on Linux, you can use the following command to join the
channel:
```cli
zed https://zed.dev/channel/zed-debugger-11370 
```

## Current Features

- Collab
  - Breakpoints
    - Sync when you (re)join a project
    - Sync when you add/remove a breakpoint
  - Sync active debug line
  - Stack frames
    - Click on stack frame
      - View variables that belong to the stack frame
      - Visit the source file
    - Restart stack frame (if adapter supports this)
  - Variables
  - Loaded sources
  - Modules
  - Controls
    - Continue
    - Step back
      - Stepping granularity (configurable)
    - Step into
      - Stepping granularity (configurable)
    - Step over
      - Stepping granularity (configurable)
    - Step out
      - Stepping granularity (configurable)
  - Debug console
- Breakpoints
  - Log breakpoints
  - line breakpoints
  - Persistent between zed sessions (configurable)
  - Multi buffer support
  - Toggle disable/enable all breakpoints
- Stack frames
  - Click on stack frame
    - View variables that belong to the stack frame
    - Visit the source file
    - Show collapsed stack frames
  - Restart stack frame (if adapter supports this)
- Loaded sources
  - View all used loaded sources if supported by adapter.
- Modules
  - View all used modules (if adapter supports this)
- Variables
  - Copy value
  - Copy name
  - Copy memory reference
  - Set value (if adapter supports this)
  - keyboard navigation
- Debug Console
  - See logs
  - View output that was sent from debug adapter
    - Output grouping
  - Evaluate code
    - Updates the variable list
    - Auto completion
- If not supported by adapter, we will show auto-completion for existing
variables
- Debug Terminal
- Run custom commands and change env values right inside your Zed
terminal
- Attach to process (if adapter supports this)
  - Process picker
- Controls
  - Continue
  - Step back
    - Stepping granularity (configurable)
  - Step into
    - Stepping granularity (configurable)
  - Step over
    - Stepping granularity (configurable)
  - Step out
    - Stepping granularity (configurable)
  - Disconnect
  - Restart
  - Stop
- Warning when a debug session exited without hitting any breakpoint
- Debug view to see Adapter/RPC log messages
- Testing
  - Fake debug adapter
    - Fake requests & events

---

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Piotr Osiewicz <peterosiewicz@gmail.com>
Co-authored-by: Piotr <piotr@zed.dev>
This commit is contained in:
Remco Smits
2025-03-18 12:55:25 -04:00
committed by GitHub
co-authored by Piotr Osiewicz Anthony Eid Anthony Piotr Osiewicz Piotr
parent ed4e654fdf
commit 41a60ffecf
156 changed files with 25840 additions and 451 deletions
@@ -0,0 +1,136 @@
use crate::*;
use attach_modal::AttachModal;
use dap::client::SessionId;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use menu::Confirm;
use project::{FakeFs, Project};
use serde_json::json;
use task::AttachConfig;
use tests::{init_test, init_test_workspace};
#[gpui::test]
async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(
dap::DebugRequestType::Attach(AttachConfig {
process_id: Some(10),
}),
None,
None,
),
cx,
)
});
let session = task.await.unwrap();
cx.run_until_parked();
// assert we didn't show the attach modal
workspace
.update(cx, |workspace, _window, cx| {
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
})
.unwrap();
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_show_attach_modal_and_select_process(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let attach_modal = workspace
.update(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::new(
project.clone(),
dap::test_config(
dap::DebugRequestType::Attach(AttachConfig { process_id: None }),
None,
None,
),
window,
cx,
)
});
workspace.active_modal::<AttachModal>(cx).unwrap()
})
.unwrap();
cx.run_until_parked();
// assert we got the expected processes
workspace
.update(cx, |_, _, cx| {
let names =
attach_modal.update(cx, |modal, cx| attach_modal::process_names(&modal, cx));
// we filtered out all processes that are not the current process(zed itself)
assert_eq!(1, names.len());
})
.unwrap();
// select the only existing process
cx.dispatch_action(Confirm);
cx.run_until_parked();
// assert attach modal was dismissed
workspace
.update(cx, |workspace, _window, cx| {
assert!(workspace.active_modal::<AttachModal>(cx).is_none());
})
.unwrap();
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
let session = dap_store.session_by_id(SessionId(0)).unwrap();
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
+916
View File
@@ -0,0 +1,916 @@
use crate::{tests::active_debug_session_panel, *};
use dap::requests::StackTrace;
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use tests::{init_test, init_test_workspace};
#[gpui::test]
async fn test_handle_output_event(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
"/project",
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
workspace
.update(cx, |workspace, window, cx| {
workspace.focus_panel::<DebugPanel>(window, cx);
})
.unwrap();
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<StackTrace, _>(move |_, _| {
Ok(dap::StackTraceResponse {
stack_frames: Vec::default(),
total_frames: None,
})
})
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: None,
output: "First console output line before thread stopped!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: Some(dap::OutputEventCategory::Stdout),
output: "First output line before thread stopped!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
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.mode()
.as_running()
.expect("Session should be running by this point")
.clone()
});
running_state.update(cx, |state, cx| {
state.set_thread_item(session::ThreadItem::Console, cx);
cx.refresh_windows();
});
cx.run_until_parked();
// assert we have output from before the thread stopped
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let active_debug_session_panel = debug_panel
.update(cx, |this, cx| this.active_session(cx))
.unwrap();
assert_eq!(
"First console output line before thread stopped!\nFirst output line before thread stopped!\n",
active_debug_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
);
})
.unwrap();
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: Some(dap::OutputEventCategory::Stdout),
output: "Second output line after thread stopped!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
client
.fake_event(dap::messages::Events::Output(dap::OutputEvent {
category: Some(dap::OutputEventCategory::Console),
output: "Second console output line after thread stopped!".to_string(),
data: None,
variables_reference: None,
source: None,
line: None,
column: None,
group: None,
location_reference: None,
}))
.await;
cx.run_until_parked();
running_state.update(cx, |state, cx| {
state.set_thread_item(session::ThreadItem::Console, cx);
cx.refresh_windows();
});
cx.run_until_parked();
// assert we have output from before and after the thread stopped
workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let active_session_panel = debug_panel
.update(cx, |this, cx| this.active_session(cx))
.unwrap();
assert_eq!(
"First console output line before thread stopped!\nFirst output line before thread stopped!\nSecond output line after thread stopped!\nSecond console output line after thread stopped!\n",
active_session_panel.read(cx).mode().as_running().unwrap().read(cx).console().read(cx).editor().read(cx).text(cx).as_str()
);
})
.unwrap();
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
// #[gpui::test]
// async fn test_grouped_output(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx);
// let fs = FakeFs::new(executor.clone());
// fs.insert_tree(
// "/project",
// json!({
// "main.rs": "First line\nSecond line\nThird line\nFourth line",
// }),
// )
// .await;
// let project = Project::test(fs, ["/project".as_ref()], cx).await;
// let workspace = init_test_workspace(&project, cx).await;
// let cx = &mut VisualTestContext::from_window(*workspace, cx);
// let task = project.update(cx, |project, cx| {
// project.start_debug_session(
// dap::test_config(dap::DebugRequestType::Launch, None, None),
// cx,
// )
// });
// let session = task.await.unwrap();
// let client = session.update(cx, |session, _| session.adapter_client().unwrap());
// client
// .on_request::<StackTrace, _>(move |_, _| {
// Ok(dap::StackTraceResponse {
// stack_frames: Vec::default(),
// total_frames: None,
// })
// })
// .await;
// 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;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: None,
// output: "First line".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "First group".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::Start),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "First item in group 1".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second item in group 1".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second group".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::Start),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "First item in group 2".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second item in group 2".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "End group 2".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::End),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Third group".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::StartCollapsed),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "First item in group 3".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second item in group 3".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "End group 3".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::End),
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Third item in group 1".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: None,
// location_reference: None,
// }))
// .await;
// client
// .fake_event(dap::messages::Events::Output(dap::OutputEvent {
// category: Some(dap::OutputEventCategory::Stdout),
// output: "Second item".to_string(),
// data: None,
// variables_reference: None,
// source: None,
// line: None,
// column: None,
// group: Some(dap::OutputEventGroup::End),
// location_reference: None,
// }))
// .await;
// cx.run_until_parked();
// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .update(cx, |running_state, cx| {
// running_state.console().update(cx, |console, cx| {
// console.editor().update(cx, |editor, cx| {
// pretty_assertions::assert_eq!(
// "
// First line
// First group
// First item in group 1
// Second item in group 1
// Second group
// First item in group 2
// Second item in group 2
// End group 2
// ⋯ End group 3
// Third item in group 1
// Second item
// "
// .unindent(),
// editor.display_text(cx)
// );
// })
// });
// });
// });
// let shutdown_session = project.update(cx, |project, cx| {
// project.dap_store().update(cx, |dap_store, cx| {
// dap_store.shutdown_session(session.read(cx).session_id(), cx)
// })
// });
// shutdown_session.await.unwrap();
// }
// todo(debugger): enable this again
// #[gpui::test]
// async fn test_evaluate_expression(executor: BackgroundExecutor, cx: &mut TestAppContext) {
// init_test(cx);
// const NEW_VALUE: &str = "{nested1: \"Nested 1 updated\", nested2: \"Nested 2 updated\"}";
// let called_evaluate = Arc::new(AtomicBool::new(false));
// 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(
// "/project",
// json!({
// "src": {
// "test.js": test_file_content,
// }
// }),
// )
// .await;
// let project = Project::test(fs, ["/project".as_ref()], cx).await;
// let workspace = init_test_workspace(&project, cx).await;
// let cx = &mut VisualTestContext::from_window(*workspace, cx);
// let task = project.update(cx, |project, cx| {
// project.start_debug_session(dap::test_config(None), cx)
// });
// let session = task.await.unwrap();
// let client = session.update(cx, |session, _| session.adapter_client().unwrap());
// client
// .on_request::<Threads, _>(move |_, _| {
// Ok(dap::ThreadsResponse {
// threads: vec![dap::Thread {
// id: 1,
// name: "Thread 1".into(),
// }],
// })
// })
// .await;
// let stack_frames = vec![StackFrame {
// id: 1,
// name: "Stack Frame 1".into(),
// source: Some(dap::Source {
// name: Some("test.js".into()),
// path: Some("/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,
// }];
// client
// .on_request::<StackTrace, _>({
// 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,
// })
// }
// })
// .await;
// 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,
// },
// 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::<Scopes, _>({
// let scopes = Arc::new(scopes.clone());
// move |_, args| {
// assert_eq!(1, args.frame_id);
// Ok(dap::ScopesResponse {
// scopes: (*scopes).clone(),
// })
// }
// })
// .await;
// let scope1_variables = Arc::new(Mutex::new(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::<Variables, _>({
// let scope1_variables = 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.lock().unwrap().clone(),
// }),
// id => unreachable!("unexpected variables reference {id}"),
// }
// })
// .await;
// client
// .on_request::<Evaluate, _>({
// let called_evaluate = called_evaluate.clone();
// let scope1_variables = scope1_variables.clone();
// move |_, args| {
// called_evaluate.store(true, Ordering::SeqCst);
// assert_eq!(format!("$variable1 = {}", NEW_VALUE), args.expression);
// assert_eq!(Some(1), args.frame_id);
// assert_eq!(Some(dap::EvaluateArgumentsContext::Variables), args.context);
// scope1_variables.lock().unwrap()[0].value = NEW_VALUE.to_string();
// Ok(dap::EvaluateResponse {
// result: NEW_VALUE.into(),
// type_: None,
// presentation_hint: None,
// variables_reference: 0,
// named_variables: None,
// indexed_variables: None,
// memory_reference: None,
// value_location_reference: None,
// })
// }
// })
// .await;
// 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();
// // toggle nested variables for scope 1
// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .update(cx, |running_state, cx| {
// running_state
// .variable_list()
// .update(cx, |variable_list, cx| {
// variable_list.toggle_variable(
// &VariablePath {
// indices: Arc::from([scopes[0].variables_reference]),
// },
// cx,
// );
// });
// });
// });
// cx.run_until_parked();
// active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .update(cx, |running_state, cx| {
// running_state.console().update(cx, |console, item_cx| {
// console
// .query_bar()
// .update(item_cx, |query_bar, console_cx| {
// query_bar.set_text(
// format!("$variable1 = {}", NEW_VALUE),
// window,
// console_cx,
// );
// });
// console.evaluate(&menu::Confirm, window, item_cx);
// });
// });
// });
// cx.run_until_parked();
// active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
// assert_eq!(
// "",
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .read(cx)
// .console()
// .read(cx)
// .query_bar()
// .read(cx)
// .text(cx)
// .as_str()
// );
// assert_eq!(
// format!("{}\n", NEW_VALUE),
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .read(cx)
// .console()
// .read(cx)
// .editor()
// .read(cx)
// .text(cx)
// .as_str()
// );
// debug_panel_item
// .mode()
// .as_running()
// .unwrap()
// .update(cx, |running_state, cx| {
// running_state
// .variable_list()
// .update(cx, |variable_list, _| {
// let scope1_variables = scope1_variables.lock().unwrap().clone();
// // scope 1
// // assert_eq!(
// // vec![
// // VariableContainer {
// // container_reference: scopes[0].variables_reference,
// // variable: scope1_variables[0].clone(),
// // depth: 1,
// // },
// // VariableContainer {
// // container_reference: scope1_variables[0].variables_reference,
// // variable: nested_variables[0].clone(),
// // depth: 2,
// // },
// // VariableContainer {
// // container_reference: scope1_variables[0].variables_reference,
// // variable: nested_variables[1].clone(),
// // depth: 2,
// // },
// // VariableContainer {
// // container_reference: scopes[0].variables_reference,
// // variable: scope1_variables[1].clone(),
// // depth: 1,
// // },
// // ],
// // variable_list.variables_by_scope(1, 2).unwrap().variables()
// // );
// // scope 2
// // assert_eq!(
// // vec![VariableContainer {
// // container_reference: scopes[1].variables_reference,
// // variable: scope2_variables[0].clone(),
// // depth: 1,
// // }],
// // variable_list.variables_by_scope(1, 4).unwrap().variables()
// // );
// variable_list.assert_visual_entries(vec![
// "v Scope 1",
// " v variable1",
// " > nested1",
// " > nested2",
// " > variable2",
// ]);
// // assert visual entries
// // assert_eq!(
// // vec![
// // VariableListEntry::Scope(scopes[0].clone()),
// // VariableListEntry::Variable {
// // depth: 1,
// // scope: Arc::new(scopes[0].clone()),
// // has_children: true,
// // variable: Arc::new(scope1_variables[0].clone()),
// // container_reference: scopes[0].variables_reference,
// // },
// // VariableListEntry::Variable {
// // depth: 2,
// // scope: Arc::new(scopes[0].clone()),
// // has_children: false,
// // variable: Arc::new(nested_variables[0].clone()),
// // container_reference: scope1_variables[0].variables_reference,
// // },
// // VariableListEntry::Variable {
// // depth: 2,
// // scope: Arc::new(scopes[0].clone()),
// // has_children: false,
// // variable: Arc::new(nested_variables[1].clone()),
// // container_reference: scope1_variables[0].variables_reference,
// // },
// // VariableListEntry::Variable {
// // depth: 1,
// // scope: Arc::new(scopes[0].clone()),
// // has_children: false,
// // variable: Arc::new(scope1_variables[1].clone()),
// // container_reference: scopes[0].variables_reference,
// // },
// // VariableListEntry::Scope(scopes[1].clone()),
// // ],
// // variable_list.entries().get(&1).unwrap().clone()
// // );
// });
// });
// });
// assert!(
// called_evaluate.load(std::sync::atomic::Ordering::SeqCst),
// "Expected evaluate request to be called"
// );
// let shutdown_session = project.update(cx, |project, cx| {
// project.dap_store().update(cx, |dap_store, cx| {
// dap_store.shutdown_session(&session.read(cx).session_id(), cx)
// })
// });
// shutdown_session.await.unwrap();
// }
File diff suppressed because it is too large Load Diff
+262
View File
@@ -0,0 +1,262 @@
use crate::{
debugger_panel::DebugPanel,
session::ThreadItem,
tests::{active_debug_session_panel, init_test, init_test_workspace},
};
use dap::{
requests::{Modules, StackTrace, Threads},
DebugRequestType, StoppedEvent,
};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use std::sync::{
atomic::{AtomicBool, AtomicI32, Ordering},
Arc,
};
#[gpui::test]
async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
let project = Project::test(fs, ["/project".as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
workspace
.update(cx, |workspace, window, cx| {
workspace.focus_panel::<DebugPanel>(window, cx);
})
.unwrap();
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(
DebugRequestType::Launch,
None,
Some(dap::Capabilities {
supports_modules_request: Some(true),
..Default::default()
}),
),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<StackTrace, _>(move |_, args| {
assert!(args.thread_id == 1);
Ok(dap::StackTraceResponse {
stack_frames: Vec::default(),
total_frames: None,
})
})
.await;
let called_modules = Arc::new(AtomicBool::new(false));
let modules = vec![
dap::Module {
id: dap::ModuleId::Number(1),
name: "First Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
},
dap::Module {
id: dap::ModuleId::Number(2),
name: "Second Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
},
];
client
.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
client
.on_request::<Modules, _>({
let called_modules = called_modules.clone();
let modules_request_count = AtomicI32::new(0);
let modules = modules.clone();
move |_, _| {
modules_request_count.fetch_add(1, Ordering::SeqCst);
assert_eq!(
1,
modules_request_count.load(Ordering::SeqCst),
"This request should only be called once from the host"
);
called_modules.store(true, Ordering::SeqCst);
Ok(dap::ModulesResponse {
modules: modules.clone(),
total_modules: Some(2u64),
})
}
})
.await;
client
.fake_event(dap::messages::Events::Stopped(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.mode()
.as_running()
.expect("Session should be running by this point")
.clone()
});
assert!(
!called_modules.load(std::sync::atomic::Ordering::SeqCst),
"Request Modules shouldn't be called before it's needed"
);
running_state.update(cx, |state, cx| {
state.set_thread_item(ThreadItem::Modules, cx);
cx.refresh_windows();
});
cx.run_until_parked();
assert!(
called_modules.load(std::sync::atomic::Ordering::SeqCst),
"Request Modules should be called because a user clicked on the module list"
);
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
running_state.update(cx, |state, cx| {
state.set_thread_item(ThreadItem::Modules, cx)
});
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(modules, actual_modules);
});
// Test all module events now
// New Module
// Changed
// Removed
let new_module = dap::Module {
id: dap::ModuleId::Number(3),
name: "Third Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
};
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::New,
module: new_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 3);
assert!(actual_modules.contains(&new_module));
});
let changed_module = dap::Module {
id: dap::ModuleId::Number(2),
name: "Modified Second Module".into(),
address_range: None,
date_time_stamp: None,
path: None,
symbol_file_path: None,
symbol_status: None,
version: None,
is_optimized: None,
is_user_code: None,
};
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::Changed,
module: changed_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 3);
assert!(actual_modules.contains(&changed_module));
});
client
.fake_event(dap::messages::Events::Module(dap::ModuleEvent {
reason: dap::ModuleEventReason::Removed,
module: changed_module.clone(),
}))
.await;
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |_, cx| {
let actual_modules = running_state.update(cx, |state, cx| {
state.module_list().update(cx, |list, cx| list.modules(cx))
});
assert_eq!(actual_modules.len(), 2);
assert!(!actual_modules.contains(&changed_module));
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
@@ -0,0 +1,845 @@
use crate::{
debugger_panel::DebugPanel,
session::running::stack_frame_list::StackFrameEntry,
tests::{active_debug_session_panel, init_test, init_test_workspace},
};
use dap::{
requests::{StackTrace, Threads},
StackFrame,
};
use editor::{Editor, ToPoint as _};
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
use project::{FakeFs, Project};
use serde_json::json;
use std::sync::Arc;
use unindent::Unindent as _;
use util::path;
#[gpui::test]
async fn test_fetch_initial_stack_frames_and_go_to_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;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
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::<StackTrace, _>({
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,
})
}
})
.await;
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();
// trigger to load threads
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state
.session()
.update(cx, |session, cx| session.threads(cx));
});
});
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state.select_current_thread(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
cx,
);
});
});
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
let stack_frame_list = session
.mode()
.as_running()
.unwrap()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_select_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;
let _ = workspace.update(cx, |workspace, window, cx| {
workspace.toggle_dock(workspace::dock::DockPosition::Bottom, window, cx);
});
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
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::<StackTrace, _>({
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,
})
}
})
.await;
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();
// trigger threads to load
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state
.session()
.update(cx, |session, cx| session.threads(cx));
});
});
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state.select_current_thread(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
cx,
);
});
});
cx.run_until_parked();
workspace
.update(cx, |workspace, window, cx| {
let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
assert_eq!(1, editors.len());
let project_path = editors[0]
.update(cx, |editor, cx| editor.project_path(cx))
.unwrap();
let expected = if cfg!(target_os = "windows") {
"src\\test.js"
} else {
"src/test.js"
};
assert_eq!(expected, project_path.path.to_string_lossy());
assert_eq!(test_file_content, editors[0].read(cx).text(cx));
assert_eq!(
vec![2..3],
editors[0].update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
editor
.highlighted_rows::<editor::DebugCurrentRowHighlight>()
.map(|(range, _)| {
let start = range.start.to_point(&snapshot.buffer_snapshot);
let end = range.end.to_point(&snapshot.buffer_snapshot);
start.row..end.row
})
.collect::<Vec<_>>()
})
);
})
.unwrap();
let stack_frame_list = workspace
.update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
let active_debug_panel_item = debug_panel
.update(cx, |this, cx| this.active_session(cx))
.unwrap();
active_debug_panel_item
.read(cx)
.mode()
.as_running()
.unwrap()
.read(cx)
.stack_frame_list()
.clone()
})
.unwrap();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
// select second stack frame
stack_frame_list
.update_in(cx, |stack_frame_list, window, cx| {
stack_frame_list.select_stack_frame(&stack_frames[1], true, window, cx)
})
.await
.unwrap();
cx.run_until_parked();
stack_frame_list.update(cx, |stack_frame_list, cx| {
assert_eq!(Some(2), stack_frame_list.current_stack_frame_id());
assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
});
let _ = workspace.update(cx, |workspace, window, cx| {
let editors = workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>();
assert_eq!(1, editors.len());
let project_path = editors[0]
.update(cx, |editor, cx| editor.project_path(cx))
.unwrap();
let expected = if cfg!(target_os = "windows") {
"src\\module.js"
} else {
"src/module.js"
};
assert_eq!(expected, project_path.path.to_string_lossy());
assert_eq!(module_file_content, editors[0].read(cx).text(cx));
assert_eq!(
vec![0..1],
editors[0].update(cx, |editor, cx| {
let snapshot = editor.snapshot(window, cx);
editor
.highlighted_rows::<editor::DebugCurrentRowHighlight>()
.map(|(range, _)| {
let start = range.start.to_point(&snapshot.buffer_snapshot);
let end = range.end.to_point(&snapshot.buffer_snapshot);
start.row..end.row
})
.collect::<Vec<_>>()
})
);
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
#[gpui::test]
async fn test_collapsed_entries(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 task = project.update(cx, |project, cx| {
project.start_debug_session(
dap::test_config(dap::DebugRequestType::Launch, None, None),
cx,
)
});
let session = task.await.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
client
.on_request::<Threads, _>(move |_, _| {
Ok(dap::ThreadsResponse {
threads: vec![dap::Thread {
id: 1,
name: "Thread 1".into(),
}],
})
})
.await;
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: Some("ignored".into()),
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: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 3,
name: "Stack Frame 3".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: Some("ignored".into()),
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: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 4,
name: "Stack Frame 4".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,
},
StackFrame {
id: 5,
name: "Stack Frame 5".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: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 6,
name: "Stack Frame 6".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: Some(dap::StackFramePresentationHint::Deemphasize),
},
StackFrame {
id: 7,
name: "Stack Frame 7".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::<StackTrace, _>({
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,
})
}
})
.await;
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();
// trigger threads to load
active_debug_session_panel(workspace, cx).update(cx, |session, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state
.session()
.update(cx, |session, cx| session.threads(cx));
});
});
cx.run_until_parked();
// select first thread
active_debug_session_panel(workspace, cx).update_in(cx, |session, _, cx| {
session
.mode()
.as_running()
.unwrap()
.update(cx, |running_state, cx| {
running_state.select_current_thread(
&running_state
.session()
.update(cx, |session, cx| session.threads(cx)),
cx,
);
});
});
cx.run_until_parked();
// trigger stack frames to loaded
active_debug_session_panel(workspace, cx).update(cx, |debug_panel_item, cx| {
let stack_frame_list = debug_panel_item
.mode()
.as_running()
.unwrap()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.dap_stack_frames(cx);
});
});
cx.run_until_parked();
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
let stack_frame_list = debug_panel_item
.mode()
.as_running()
.unwrap()
.update(cx, |state, _| state.stack_frame_list().clone());
stack_frame_list.update(cx, |stack_frame_list, cx| {
stack_frame_list.build_entries(true, window, cx);
assert_eq!(
&vec![
StackFrameEntry::Normal(stack_frames[0].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames[1].clone(),
stack_frames[2].clone()
]),
StackFrameEntry::Normal(stack_frames[3].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames[4].clone(),
stack_frames[5].clone()
]),
StackFrameEntry::Normal(stack_frames[6].clone()),
],
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(
1,
&vec![stack_frames[1].clone(), stack_frames[2].clone()],
cx,
);
assert_eq!(
&vec![
StackFrameEntry::Normal(stack_frames[0].clone()),
StackFrameEntry::Normal(stack_frames[1].clone()),
StackFrameEntry::Normal(stack_frames[2].clone()),
StackFrameEntry::Normal(stack_frames[3].clone()),
StackFrameEntry::Collapsed(vec![
stack_frames[4].clone(),
stack_frames[5].clone()
]),
StackFrameEntry::Normal(stack_frames[6].clone()),
],
stack_frame_list.entries()
);
stack_frame_list.expand_collapsed_entry(
4,
&vec![stack_frames[4].clone(), stack_frames[5].clone()],
cx,
);
assert_eq!(
&vec![
StackFrameEntry::Normal(stack_frames[0].clone()),
StackFrameEntry::Normal(stack_frames[1].clone()),
StackFrameEntry::Normal(stack_frames[2].clone()),
StackFrameEntry::Normal(stack_frames[3].clone()),
StackFrameEntry::Normal(stack_frames[4].clone()),
StackFrameEntry::Normal(stack_frames[5].clone()),
StackFrameEntry::Normal(stack_frames[6].clone()),
],
stack_frame_list.entries()
);
});
});
let shutdown_session = project.update(cx, |project, cx| {
project.dap_store().update(cx, |dap_store, cx| {
dap_store.shutdown_session(session.read(cx).session_id(), cx)
})
});
shutdown_session.await.unwrap();
}
File diff suppressed because it is too large Load Diff