debugger: Update New Session Modal (#30018)

This PR simplifies the new session modal by flattening its three modes
and updating the UI to be less noisy. The new UI also defaults to the
Debug Scenario Picker, and allows users to save debug scenarios created
in the UI to the active worktree's .zed/debug.json file.


Release Notes:

- N/A
This commit is contained in:
Anthony Eid 2025-05-08 18:19:14 +02:00 committed by GitHub
parent e9a756b5fc
commit dc01aef0cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 460 additions and 527 deletions

1
Cargo.lock generated
View file

@ -4199,6 +4199,7 @@ dependencies = [
"log", "log",
"menu", "menu",
"parking_lot", "parking_lot",
"paths",
"picker", "picker",
"pretty_assertions", "pretty_assertions",
"project", "project",

View file

@ -43,6 +43,7 @@ language.workspace = true
log.workspace = true log.workspace = true
menu.workspace = true menu.workspace = true
parking_lot.workspace = true parking_lot.workspace = true
paths.workspace = true
picker.workspace = true picker.workspace = true
pretty_assertions.workspace = true pretty_assertions.workspace = true
project.workspace = true project.workspace = true

View file

@ -32,12 +32,12 @@ pub(crate) struct AttachModalDelegate {
impl AttachModalDelegate { impl AttachModalDelegate {
fn new( fn new(
workspace: Entity<Workspace>, workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition, definition: DebugTaskDefinition,
candidates: Arc<[Candidate]>, candidates: Arc<[Candidate]>,
) -> Self { ) -> Self {
Self { Self {
workspace: workspace.downgrade(), workspace,
definition, definition,
candidates, candidates,
selected_index: 0, selected_index: 0,
@ -55,7 +55,7 @@ pub struct AttachModal {
impl AttachModal { impl AttachModal {
pub fn new( pub fn new(
definition: DebugTaskDefinition, definition: DebugTaskDefinition,
workspace: Entity<Workspace>, workspace: WeakEntity<Workspace>,
modal: bool, modal: bool,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@ -82,7 +82,7 @@ impl AttachModal {
} }
pub(super) fn with_processes( pub(super) fn with_processes(
workspace: Entity<Workspace>, workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition, definition: DebugTaskDefinition,
processes: Arc<[Candidate]>, processes: Arc<[Candidate]>,
modal: bool, modal: bool,

View file

@ -5,15 +5,15 @@ use crate::{
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, StepBack, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, StepBack,
StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
}; };
use anyhow::Result; use anyhow::{Result, anyhow};
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use dap::StartDebuggingRequestArguments;
use dap::adapters::DebugAdapterName; use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition; use dap::debugger_settings::DebugPanelDockPosition;
use dap::{ use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings, client::SessionId, debugger_settings::DebuggerSettings,
}; };
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::{ use gpui::{
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter, Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
@ -54,12 +54,11 @@ pub enum DebugPanelEvent {
} }
actions!(debug_panel, [ToggleFocus]); actions!(debug_panel, [ToggleFocus]);
pub struct DebugPanel { pub struct DebugPanel {
size: Pixels, size: Pixels,
sessions: Vec<Entity<DebugSession>>, sessions: Vec<Entity<DebugSession>>,
active_session: Option<Entity<DebugSession>>, 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: Entity<Project>, project: Entity<Project>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
@ -80,7 +79,6 @@ impl DebugPanel {
size: px(300.), size: px(300.),
sessions: vec![], sessions: vec![],
active_session: None, active_session: None,
past_debug_definition: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
project, project,
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
@ -992,6 +990,69 @@ impl DebugPanel {
self.active_session = Some(session_item); self.active_session = Some(session_item);
cx.notify(); cx.notify();
} }
pub(crate) fn save_scenario(
&self,
scenario: &DebugScenario,
worktree_id: WorktreeId,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
self.workspace
.update(cx, |workspace, cx| {
let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
return Task::ready(Err(anyhow!("Couldn't get worktree path")));
};
let serialized_scenario = serde_json::to_value(scenario);
path.push(paths::local_debug_file_relative_path());
cx.spawn_in(window, async move |workspace, cx| {
let serialized_scenario = serialized_scenario?;
let path = path.as_path();
let fs =
workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
if !fs.is_file(path).await {
let content =
serde_json::to_string_pretty(&serde_json::Value::Array(vec![
serialized_scenario,
]))?;
fs.create_file(path, Default::default()).await?;
fs.save(path, &content.into(), Default::default()).await?;
} else {
let content = fs.load(path).await?;
let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
values.push(serialized_scenario);
fs.save(
path,
&serde_json::to_string_pretty(&values).map(Into::into)?,
Default::default(),
)
.await?;
}
workspace.update_in(cx, |workspace, window, cx| {
if let Some(project_path) = workspace
.project()
.read(cx)
.project_path_for_absolute_path(&path, cx)
{
workspace.open_path(project_path, None, true, window, cx)
} else {
Task::ready(Err(anyhow!(
"Couldn't get project path for .zed/debug.json in active worktree"
)))
}
})?.await?;
anyhow::Ok(())
})
})
.unwrap_or_else(|err| Task::ready(Err(err)))
}
} }
impl EventEmitter<PanelEvent> for DebugPanel {} impl EventEmitter<PanelEvent> for DebugPanel {}

View file

@ -147,36 +147,7 @@ pub fn init(cx: &mut App) {
}, },
) )
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| { .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) { NewSessionModal::show(workspace, window, cx);
let weak_panel = debug_panel.downgrade();
let weak_workspace = cx.weak_entity();
let task_store = workspace.project().read(cx).task_store().clone();
cx.spawn_in(window, async move |this, cx| {
let task_contexts = this
.update_in(cx, |workspace, window, cx| {
tasks_ui::task_contexts(workspace, window, cx)
})?
.await;
this.update_in(cx, |workspace, window, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
NewSessionModal::new(
debug_panel.read(cx).past_debug_definition.clone(),
weak_panel,
weak_workspace,
Some(task_store),
task_contexts,
window,
cx,
)
});
})?;
anyhow::Ok(())
})
.detach()
}
}); });
}) })
}) })

File diff suppressed because it is too large Load diff

View file

@ -864,7 +864,7 @@ impl RunningState {
dap::DebugRequest::Launch(new_launch_request) dap::DebugRequest::Launch(new_launch_request)
} }
request @ dap::DebugRequest::Attach(_) => request, request @ dap::DebugRequest::Attach(_) => request, // todo(debugger): We should check that process_id is valid and if not show the modal
}; };
Ok(DebugTaskDefinition { Ok(DebugTaskDefinition {
label, label,

View file

@ -103,7 +103,7 @@ async fn test_show_attach_modal_and_select_process(
}); });
let attach_modal = workspace let attach_modal = workspace
.update(cx, |workspace, window, cx| { .update(cx, |workspace, window, cx| {
let workspace_handle = cx.entity(); let workspace_handle = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| { workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes( AttachModal::with_processes(
workspace_handle, workspace_handle,

View file

@ -141,7 +141,7 @@ impl JsonLspAdapter {
}, },
{ {
"fileMatch": [ "fileMatch": [
schema_file_match(paths::debug_tasks_file()), schema_file_match(paths::debug_scenarios_file()),
paths::local_debug_file_relative_path() paths::local_debug_file_relative_path()
], ],
"schema": debug_schema, "schema": debug_schema,

View file

@ -216,9 +216,9 @@ pub fn tasks_file() -> &'static PathBuf {
} }
/// Returns the path to the `debug.json` file. /// Returns the path to the `debug.json` file.
pub fn debug_tasks_file() -> &'static PathBuf { pub fn debug_scenarios_file() -> &'static PathBuf {
static DEBUG_TASKS_FILE: OnceLock<PathBuf> = OnceLock::new(); static DEBUG_SCENARIOS_FILE: OnceLock<PathBuf> = OnceLock::new();
DEBUG_TASKS_FILE.get_or_init(|| config_dir().join("debug.json")) DEBUG_SCENARIOS_FILE.get_or_init(|| config_dir().join("debug.json"))
} }
/// Returns the path to the extensions directory. /// Returns the path to the extensions directory.

View file

@ -193,22 +193,22 @@ pub struct DebugScenario {
/// Name of the debug task /// Name of the debug task
pub label: SharedString, pub label: SharedString,
/// A task to run prior to spawning the debuggee. /// A task to run prior to spawning the debuggee.
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build: Option<BuildTaskDefinition>, pub build: Option<BuildTaskDefinition>,
#[serde(flatten)] #[serde(flatten)]
pub request: Option<DebugRequest>, pub request: Option<DebugRequest>,
/// Additional initialization arguments to be sent on DAP initialization /// Additional initialization arguments to be sent on DAP initialization
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub initialize_args: Option<serde_json::Value>, pub initialize_args: Option<serde_json::Value>,
/// Optional TCP connection information /// Optional TCP connection information
/// ///
/// If provided, this will be used to connect to the debug adapter instead of /// If provided, this will be used to connect to the debug adapter instead of
/// spawning a new process. This is useful for connecting to a debug adapter /// spawning a new process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process. /// that is already running or is started by another process.
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub tcp_connection: Option<TcpArgumentsTemplate>, pub tcp_connection: Option<TcpArgumentsTemplate>,
/// Whether to tell the debug adapter to stop on entry /// Whether to tell the debug adapter to stop on entry
#[serde(default)] #[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_on_entry: Option<bool>, pub stop_on_entry: Option<bool>,
} }

View file

@ -701,7 +701,7 @@ fn register_actions(
}) })
.register_action(move |_: &mut Workspace, _: &OpenDebugTasks, window, cx| { .register_action(move |_: &mut Workspace, _: &OpenDebugTasks, window, cx| {
open_settings_file( open_settings_file(
paths::debug_tasks_file(), paths::debug_scenarios_file(),
|| settings::initial_debug_tasks_content().as_ref().into(), || settings::initial_debug_tasks_content().as_ref().into(),
window, window,
cx, cx,