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
536
crates/debugger_ui/src/debugger_panel.rs
Normal file
536
crates/debugger_ui/src/debugger_panel.rs
Normal file
|
@ -0,0 +1,536 @@
|
|||
use crate::session::DebugSession;
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use dap::{
|
||||
client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
|
||||
ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
|
||||
};
|
||||
use futures::{channel::mpsc, SinkExt as _};
|
||||
use gpui::{
|
||||
actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use project::{
|
||||
debugger::dap_store::{self, DapStore},
|
||||
terminals::TerminalKind,
|
||||
Project,
|
||||
};
|
||||
use rpc::proto::{self};
|
||||
use settings::Settings;
|
||||
use std::{any::TypeId, path::PathBuf};
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use ui::prelude::*;
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop,
|
||||
ToggleIgnoreBreakpoints, Workspace,
|
||||
};
|
||||
|
||||
pub enum DebugPanelEvent {
|
||||
Exited(SessionId),
|
||||
Terminated(SessionId),
|
||||
Stopped {
|
||||
client_id: SessionId,
|
||||
event: StoppedEvent,
|
||||
go_to_stack_frame: bool,
|
||||
},
|
||||
Thread((SessionId, ThreadEvent)),
|
||||
Continued((SessionId, ContinuedEvent)),
|
||||
Output((SessionId, OutputEvent)),
|
||||
Module((SessionId, ModuleEvent)),
|
||||
LoadedSource((SessionId, LoadedSourceEvent)),
|
||||
ClientShutdown(SessionId),
|
||||
CapabilitiesChanged(SessionId),
|
||||
}
|
||||
|
||||
actions!(debug_panel, [ToggleFocus]);
|
||||
pub struct DebugPanel {
|
||||
size: Pixels,
|
||||
pane: Entity<Pane>,
|
||||
project: WeakEntity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl DebugPanel {
|
||||
pub fn new(
|
||||
workspace: &Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let project = workspace.project().clone();
|
||||
let dap_store = project.read(cx).dap_store();
|
||||
let weak_workspace = workspace.weak_handle();
|
||||
let pane = cx.new(|cx| {
|
||||
let mut pane = Pane::new(
|
||||
workspace.weak_handle(),
|
||||
project.clone(),
|
||||
Default::default(),
|
||||
None,
|
||||
gpui::NoAction.boxed_clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pane.set_can_split(None);
|
||||
pane.set_can_navigate(true, cx);
|
||||
pane.display_nav_history_buttons(None);
|
||||
pane.set_should_display_tab_bar(|_window, _cx| true);
|
||||
pane.set_close_pane_if_empty(true, cx);
|
||||
pane.set_render_tab_bar_buttons(cx, {
|
||||
let project = project.clone();
|
||||
let weak_workspace = weak_workspace.clone();
|
||||
move |_, _, cx| {
|
||||
let project = project.clone();
|
||||
let weak_workspace = weak_workspace.clone();
|
||||
(
|
||||
None,
|
||||
Some(
|
||||
h_flex()
|
||||
.child(
|
||||
IconButton::new("new-debug-session", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(move |pane, _, window, cx| {
|
||||
pane.add_item(
|
||||
Box::new(DebugSession::inert(
|
||||
project.clone(),
|
||||
weak_workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
)
|
||||
}
|
||||
});
|
||||
pane.add_item(
|
||||
Box::new(DebugSession::inert(
|
||||
project.clone(),
|
||||
weak_workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
pane
|
||||
});
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.observe(&pane, |_, _, cx| cx.notify()),
|
||||
cx.subscribe_in(&pane, window, Self::handle_pane_event),
|
||||
cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
|
||||
];
|
||||
|
||||
let debug_panel = Self {
|
||||
pane,
|
||||
size: px(300.),
|
||||
_subscriptions,
|
||||
project: project.downgrade(),
|
||||
workspace: workspace.weak_handle(),
|
||||
};
|
||||
|
||||
debug_panel
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
let debug_panel = DebugPanel::new(workspace, window, cx);
|
||||
|
||||
cx.observe(&debug_panel, |_, debug_panel, cx| {
|
||||
let (has_active_session, supports_restart, support_step_back) = debug_panel
|
||||
.update(cx, |this, cx| {
|
||||
this.active_session(cx)
|
||||
.map(|item| {
|
||||
let running = item.read(cx).mode().as_running().cloned();
|
||||
|
||||
match running {
|
||||
Some(running) => {
|
||||
let caps = running.read(cx).capabilities(cx);
|
||||
(
|
||||
true,
|
||||
caps.supports_restart_request.unwrap_or_default(),
|
||||
caps.supports_step_back.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
None => (false, false, false),
|
||||
}
|
||||
})
|
||||
.unwrap_or((false, false, false))
|
||||
});
|
||||
|
||||
let filter = CommandPaletteFilter::global_mut(cx);
|
||||
let debugger_action_types = [
|
||||
TypeId::of::<Continue>(),
|
||||
TypeId::of::<StepOver>(),
|
||||
TypeId::of::<StepInto>(),
|
||||
TypeId::of::<StepOut>(),
|
||||
TypeId::of::<Stop>(),
|
||||
TypeId::of::<Disconnect>(),
|
||||
TypeId::of::<Pause>(),
|
||||
TypeId::of::<ToggleIgnoreBreakpoints>(),
|
||||
];
|
||||
|
||||
let step_back_action_type = [TypeId::of::<StepBack>()];
|
||||
let restart_action_type = [TypeId::of::<Restart>()];
|
||||
|
||||
if has_active_session {
|
||||
filter.show_action_types(debugger_action_types.iter());
|
||||
|
||||
if supports_restart {
|
||||
filter.show_action_types(restart_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
}
|
||||
|
||||
if support_step_back {
|
||||
filter.show_action_types(step_back_action_type.iter());
|
||||
} else {
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
}
|
||||
} else {
|
||||
// show only the `debug: start`
|
||||
filter.hide_action_types(&debugger_action_types);
|
||||
filter.hide_action_types(&step_back_action_type);
|
||||
filter.hide_action_types(&restart_action_type);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
debug_panel
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> {
|
||||
self.pane
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|panel| panel.downcast::<DebugSession>())
|
||||
}
|
||||
|
||||
pub fn debug_panel_items_by_client(
|
||||
&self,
|
||||
client_id: &SessionId,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<Entity<DebugSession>> {
|
||||
self.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.filter_map(|item| item.downcast::<DebugSession>())
|
||||
.filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
|
||||
.map(|item| item.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn debug_panel_item_by_client(
|
||||
&self,
|
||||
client_id: SessionId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<DebugSession>> {
|
||||
self.pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.filter_map(|item| item.downcast::<DebugSession>())
|
||||
.find(|item| {
|
||||
let item = item.read(cx);
|
||||
|
||||
item.session_id(cx) == Some(client_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_dap_store_event(
|
||||
&mut self,
|
||||
dap_store: &Entity<DapStore>,
|
||||
event: &dap_store::DapStoreEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
|
||||
let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
|
||||
return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event");
|
||||
};
|
||||
|
||||
let Some(project) = self.project.upgrade() else {
|
||||
return log::error!("Debug Panel out lived it's weak reference to Project");
|
||||
};
|
||||
|
||||
if self.pane.read_with(cx, |pane, cx| {
|
||||
pane.items_of_type::<DebugSession>()
|
||||
.any(|item| item.read(cx).session_id(cx) == Some(*session_id))
|
||||
}) {
|
||||
// We already have an item for this session.
|
||||
return;
|
||||
}
|
||||
let session_item =
|
||||
DebugSession::running(project, self.workspace.clone(), session, window, cx);
|
||||
|
||||
self.pane.update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(session_item), true, true, None, window, cx);
|
||||
window.focus(&pane.focus_handle(cx));
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
dap_store::DapStoreEvent::RunInTerminal {
|
||||
title,
|
||||
cwd,
|
||||
command,
|
||||
args,
|
||||
envs,
|
||||
sender,
|
||||
..
|
||||
} => {
|
||||
self.handle_run_in_terminal_request(
|
||||
title.clone(),
|
||||
cwd.clone(),
|
||||
command.clone(),
|
||||
args.clone(),
|
||||
envs.clone(),
|
||||
sender.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_run_in_terminal_request(
|
||||
&self,
|
||||
title: Option<String>,
|
||||
cwd: PathBuf,
|
||||
command: Option<String>,
|
||||
args: Vec<String>,
|
||||
envs: HashMap<String, String>,
|
||||
mut sender: mpsc::Sender<Result<u32>>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let terminal_task = self.workspace.update(cx, |workspace, cx| {
|
||||
let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
|
||||
anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
|
||||
});
|
||||
|
||||
let terminal_panel = match terminal_panel {
|
||||
Ok(panel) => panel,
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
|
||||
terminal_panel.update(cx, |terminal_panel, cx| {
|
||||
let terminal_task = terminal_panel.add_terminal(
|
||||
TerminalKind::Debug {
|
||||
command,
|
||||
args,
|
||||
envs,
|
||||
cwd,
|
||||
title,
|
||||
},
|
||||
task::RevealStrategy::Always,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let pid_task = async move {
|
||||
let terminal = terminal_task.await?;
|
||||
|
||||
terminal.read_with(&mut cx, |terminal, _| terminal.pty_info.pid())
|
||||
};
|
||||
|
||||
pid_task.await
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
match terminal_task {
|
||||
Ok(pid_task) => match pid_task.await {
|
||||
Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
|
||||
Ok(None) => {
|
||||
sender
|
||||
.send(Err(anyhow!(
|
||||
"Terminal was spawned but PID was not available"
|
||||
)))
|
||||
.await?
|
||||
}
|
||||
Err(error) => sender.send(Err(anyhow!(error))).await?,
|
||||
},
|
||||
Err(error) => sender.send(Err(anyhow!(error))).await?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_pane_event(
|
||||
&mut self,
|
||||
_: &Entity<Pane>,
|
||||
event: &pane::Event,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
|
||||
pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
|
||||
pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
|
||||
pane::Event::AddItem { item } => {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
item.added_to_pane(workspace, self.pane.clone(), window, cx)
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
pane::Event::RemovedItem { item } => {
|
||||
if let Some(debug_session) = item.downcast::<DebugSession>() {
|
||||
debug_session.update(cx, |session, cx| {
|
||||
session.shutdown(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
pane::Event::ActivateItem {
|
||||
local: _,
|
||||
focus_changed,
|
||||
} => {
|
||||
if *focus_changed {
|
||||
if let Some(debug_session) = self
|
||||
.pane
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<DebugSession>())
|
||||
{
|
||||
if let Some(running) = debug_session
|
||||
.read_with(cx, |session, _| session.mode().as_running().cloned())
|
||||
{
|
||||
running.update(cx, |running, cx| {
|
||||
running.go_to_selected_stack_frame(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<project::Event> for DebugPanel {}
|
||||
|
||||
impl Focusable for DebugPanel {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.pane.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for DebugPanel {
|
||||
fn pane(&self) -> Option<Entity<Pane>> {
|
||||
Some(self.pane.clone())
|
||||
}
|
||||
|
||||
fn persistent_name() -> &'static str {
|
||||
"DebugPanel"
|
||||
}
|
||||
|
||||
fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
|
||||
DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
position == DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn set_position(
|
||||
&mut self,
|
||||
_position: DockPosition,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn size(&self, _window: &Window, _cx: &App) -> Pixels {
|
||||
self.size
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
|
||||
self.size = size.unwrap();
|
||||
}
|
||||
|
||||
fn remote_id() -> Option<proto::PanelId> {
|
||||
Some(proto::PanelId::DebugPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
|
||||
Some(IconName::Debug)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
|
||||
if DebuggerSettings::get_global(cx).button {
|
||||
Some("Debug Panel")
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
9
|
||||
}
|
||||
fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if active && self.pane.read(cx).items_len() == 0 {
|
||||
let Some(project) = self.project.clone().upgrade() else {
|
||||
return;
|
||||
};
|
||||
// todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
|
||||
self.pane.update(cx, |this, cx| {
|
||||
this.add_item(
|
||||
Box::new(DebugSession::inert(
|
||||
project,
|
||||
self.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for DebugPanel {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("DebugPanel")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.child(self.pane.clone())
|
||||
.into_any()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue