debugger: Open debugger panel on session startup (#29186)
Now all debug sessions are routed through the debug panel and are started synchronously instead of by a task that returns a session once the initialization process is finished. A session is `Mode::Booting` while it's starting the debug adapter process and then transitions to `Mode::Running` once this is completed. This PR also added new tests for the dap logger, reverse start debugging request, and debugging over SSH. Release Notes: - N/A --------- Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Anthony <anthony@zed.dev> Co-authored-by: Cole Miller <m@cole-miller.net> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Zed AI <ai@zed.dev> Co-authored-by: Remco Smits <djsmits12@gmail.com>
This commit is contained in:
parent
75ab8ff9a1
commit
6a009b447a
29 changed files with 1261 additions and 1021 deletions
|
@ -6,6 +6,7 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession};
|
|||
use anyhow::{Result, anyhow};
|
||||
use collections::HashMap;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use dap::StartDebuggingRequestArguments;
|
||||
use dap::{
|
||||
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
|
||||
client::SessionId, debugger_settings::DebuggerSettings,
|
||||
|
@ -17,6 +18,7 @@ use gpui::{
|
|||
actions, anchored, deferred,
|
||||
};
|
||||
|
||||
use project::debugger::session::{Session, SessionStateEvent};
|
||||
use project::{
|
||||
Project,
|
||||
debugger::{
|
||||
|
@ -30,10 +32,9 @@ use settings::Settings;
|
|||
use std::any::TypeId;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use task::DebugTaskDefinition;
|
||||
use task::{DebugTaskDefinition, DebugTaskTemplate};
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
|
||||
use util::debug_panic;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
|
@ -63,7 +64,7 @@ pub struct DebugPanel {
|
|||
active_session: Option<Entity<DebugSession>>,
|
||||
/// This represents the last debug definition that was created in the new session modal
|
||||
pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
|
||||
project: WeakEntity<Project>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
|
@ -97,10 +98,10 @@ impl DebugPanel {
|
|||
window,
|
||||
|panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
|
||||
panel.workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let workspace_handle = cx.entity().clone();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
crate::attach_modal::AttachModal::new(
|
||||
project,
|
||||
workspace_handle,
|
||||
event.debug_config.clone(),
|
||||
true,
|
||||
window,
|
||||
|
@ -127,7 +128,7 @@ impl DebugPanel {
|
|||
_subscriptions,
|
||||
past_debug_definition: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
project: project.downgrade(),
|
||||
project,
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
};
|
||||
|
@ -219,7 +220,7 @@ impl DebugPanel {
|
|||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
cx.spawn(async move |cx| {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
|
@ -245,114 +246,226 @@ impl DebugPanel {
|
|||
});
|
||||
})
|
||||
.detach();
|
||||
workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
|
||||
|
||||
debug_panel
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_session(
|
||||
&mut self,
|
||||
definition: DebugTaskDefinition,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let task_contexts = self
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})
|
||||
.ok();
|
||||
let dap_store = self.project.read(cx).dap_store().clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_context = if let Some(task) = task_contexts {
|
||||
task.await
|
||||
.active_worktree_context
|
||||
.map_or(task::TaskContext::default(), |context| context.1)
|
||||
} else {
|
||||
task::TaskContext::default()
|
||||
};
|
||||
|
||||
let (session, task) = dap_store.update(cx, |dap_store, cx| {
|
||||
let template = DebugTaskTemplate {
|
||||
locator: None,
|
||||
definition: definition.clone(),
|
||||
};
|
||||
let session = if let Some(debug_config) = template
|
||||
.to_zed_format()
|
||||
.resolve_task("debug_task", &task_context)
|
||||
.and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
|
||||
{
|
||||
dap_store.new_session(debug_config.definition, None, cx)
|
||||
} else {
|
||||
dap_store.new_session(definition.clone(), None, cx)
|
||||
};
|
||||
|
||||
(session.clone(), dap_store.boot_session(session, cx))
|
||||
})?;
|
||||
|
||||
match task.await {
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_error(&e, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
|
||||
session
|
||||
.update(cx, |session, cx| session.shutdown(cx))?
|
||||
.await;
|
||||
}
|
||||
Ok(_) => Self::register_session(this, session, cx).await?,
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
async fn register_session(
|
||||
this: WeakEntity<Self>,
|
||||
session: Entity<Session>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
let adapter_name = session.update(cx, |session, _| session.adapter_name())?;
|
||||
this.update_in(cx, |_, window, cx| {
|
||||
cx.subscribe_in(
|
||||
&session,
|
||||
window,
|
||||
move |_, session, event: &SessionStateEvent, window, cx| match event {
|
||||
SessionStateEvent::Restart => {
|
||||
let mut curr_session = session.clone();
|
||||
while let Some(parent_session) = curr_session
|
||||
.read_with(cx, |session, _| session.parent_session().cloned())
|
||||
{
|
||||
curr_session = parent_session;
|
||||
}
|
||||
|
||||
let definition = curr_session.update(cx, |session, _| session.definition());
|
||||
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
|
||||
|
||||
let definition = definition.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
task.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.start_session(definition, window, cx)
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
})
|
||||
.ok();
|
||||
|
||||
let serialized_layout = persistence::get_serialized_pane_layout(adapter_name).await;
|
||||
|
||||
let workspace = this.update_in(cx, |this, window, cx| {
|
||||
this.sessions.retain(|session| {
|
||||
session
|
||||
.read(cx)
|
||||
.mode()
|
||||
.as_running()
|
||||
.map_or(false, |running_state| {
|
||||
!running_state.read(cx).session().read(cx).is_terminated()
|
||||
})
|
||||
});
|
||||
|
||||
let session_item = DebugSession::running(
|
||||
this.project.clone(),
|
||||
this.workspace.clone(),
|
||||
session,
|
||||
cx.weak_entity(),
|
||||
serialized_layout,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
|
||||
// We might want to make this an event subscription and only notify when a new thread is selected
|
||||
// This is used to filter the command menu correctly
|
||||
cx.observe(&running, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
this.sessions.push(session_item.clone());
|
||||
this.activate_session(session_item, window, cx);
|
||||
this.workspace.clone()
|
||||
})?;
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.focus_panel::<Self>(window, cx);
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_child_session(
|
||||
&mut self,
|
||||
request: &StartDebuggingRequestArguments,
|
||||
parent_session: Entity<Session>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(worktree) = parent_session.read(cx).worktree() else {
|
||||
log::error!("Attempted to start a child session from non local debug session");
|
||||
return;
|
||||
};
|
||||
|
||||
let dap_store_handle = self.project.read(cx).dap_store().clone();
|
||||
let breakpoint_store = self.project.read(cx).breakpoint_store();
|
||||
let definition = parent_session.read(cx).definition().clone();
|
||||
let mut binary = parent_session.read(cx).binary().clone();
|
||||
binary.request_args = request.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
|
||||
let session =
|
||||
dap_store.new_session(definition.clone(), Some(parent_session.clone()), cx);
|
||||
|
||||
let task = session.update(cx, |session, cx| {
|
||||
session.boot(
|
||||
binary,
|
||||
worktree,
|
||||
breakpoint_store,
|
||||
dap_store_handle.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
(session, task)
|
||||
})?;
|
||||
|
||||
match task.await {
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.show_error(&e, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
|
||||
session
|
||||
.update(cx, |session, cx| session.shutdown(cx))?
|
||||
.await;
|
||||
}
|
||||
Ok(_) => Self::register_session(this, session, cx).await?,
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn active_session(&self) -> Option<Entity<DebugSession>> {
|
||||
self.active_session.clone()
|
||||
}
|
||||
|
||||
pub fn debug_panel_items_by_client(
|
||||
&self,
|
||||
client_id: &SessionId,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<Entity<DebugSession>> {
|
||||
self.sessions
|
||||
.iter()
|
||||
.filter(|item| item.read(cx).session_id(cx) == *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.sessions
|
||||
.iter()
|
||||
.find(|item| {
|
||||
let item = item.read(cx);
|
||||
|
||||
item.session_id(cx) == client_id
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn handle_dap_store_event(
|
||||
&mut self,
|
||||
dap_store: &Entity<DapStore>,
|
||||
_dap_store: &Entity<DapStore>,
|
||||
event: &dap_store::DapStoreEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
dap_store::DapStoreEvent::DebugSessionInitialized(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 adapter_name = session.read(cx).adapter_name();
|
||||
|
||||
let session_id = *session_id;
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let serialized_layout =
|
||||
persistence::get_serialized_pane_layout(adapter_name).await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let Some(project) = this.project.upgrade() else {
|
||||
return log::error!(
|
||||
"Debug Panel out lived it's weak reference to Project"
|
||||
);
|
||||
};
|
||||
|
||||
if this
|
||||
.sessions
|
||||
.iter()
|
||||
.any(|item| item.read(cx).session_id(cx) == session_id)
|
||||
{
|
||||
// We already have an item for this session.
|
||||
debug_panic!("We should never reuse session ids");
|
||||
return;
|
||||
}
|
||||
|
||||
this.sessions.retain(|session| {
|
||||
session
|
||||
.read(cx)
|
||||
.mode()
|
||||
.as_running()
|
||||
.map_or(false, |running_state| {
|
||||
!running_state.read(cx).session().read(cx).is_terminated()
|
||||
})
|
||||
});
|
||||
|
||||
let session_item = DebugSession::running(
|
||||
project,
|
||||
this.workspace.clone(),
|
||||
session,
|
||||
cx.weak_entity(),
|
||||
serialized_layout,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
|
||||
// We might want to make this an event subscription and only notify when a new thread is selected
|
||||
// This is used to filter the command menu correctly
|
||||
cx.observe(&running, |_, _, cx| cx.notify()).detach();
|
||||
}
|
||||
|
||||
this.sessions.push(session_item.clone());
|
||||
this.activate_session(session_item, window, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
dap_store::DapStoreEvent::RunInTerminal {
|
||||
title,
|
||||
cwd,
|
||||
|
@ -374,6 +487,12 @@ impl DebugPanel {
|
|||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
dap_store::DapStoreEvent::SpawnChildSession {
|
||||
request,
|
||||
parent_session,
|
||||
} => {
|
||||
self.start_child_session(request, parent_session.clone(), window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -408,7 +527,7 @@ impl DebugPanel {
|
|||
cwd,
|
||||
title,
|
||||
},
|
||||
task::RevealStrategy::Always,
|
||||
task::RevealStrategy::Never,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
@ -468,8 +587,6 @@ impl DebugPanel {
|
|||
let session = this.dap_store().read(cx).session_by_id(session_id);
|
||||
session.map(|session| !session.read(cx).is_terminated())
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
|
@ -893,7 +1010,6 @@ impl DebugPanel {
|
|||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<project::Event> for DebugPanel {}
|
||||
|
||||
impl Focusable for DebugPanel {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
|
@ -1039,3 +1155,15 @@ impl Render for DebugPanel {
|
|||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
struct DebuggerProvider(Entity<DebugPanel>);
|
||||
|
||||
impl workspace::DebuggerProvider for DebuggerProvider {
|
||||
fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
|
||||
self.0.update(cx, |_, cx| {
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.start_session(definition, window, cx);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue