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:
parent
ed4e654fdf
commit
41a60ffecf
156 changed files with 25840 additions and 451 deletions
262
crates/debugger_ui/src/tests/module_list.rs
Normal file
262
crates/debugger_ui/src/tests/module_list.rs
Normal 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();
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue