From dc01aef0cf9c204c5d0dbfdabdb3b761441f06e5 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 8 May 2025 18:19:14 +0200 Subject: [PATCH] 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 --- Cargo.lock | 1 + crates/debugger_ui/Cargo.toml | 1 + crates/debugger_ui/src/attach_modal.rs | 8 +- crates/debugger_ui/src/debugger_panel.rs | 71 +- crates/debugger_ui/src/debugger_ui.rs | 31 +- crates/debugger_ui/src/new_session_modal.rs | 853 ++++++++----------- crates/debugger_ui/src/session/running.rs | 2 +- crates/debugger_ui/src/tests/attach_modal.rs | 2 +- crates/languages/src/json.rs | 2 +- crates/paths/src/paths.rs | 6 +- crates/task/src/debug_format.rs | 8 +- crates/zed/src/zed.rs | 2 +- 12 files changed, 460 insertions(+), 527 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e83046c42..bf6bc016d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4199,6 +4199,7 @@ dependencies = [ "log", "menu", "parking_lot", + "paths", "picker", "pretty_assertions", "project", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 295d18e4b3..dfc1500991 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -43,6 +43,7 @@ language.workspace = true log.workspace = true menu.workspace = true parking_lot.workspace = true +paths.workspace = true picker.workspace = true pretty_assertions.workspace = true project.workspace = true diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index e054985f92..9575ff546d 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -32,12 +32,12 @@ pub(crate) struct AttachModalDelegate { impl AttachModalDelegate { fn new( - workspace: Entity, + workspace: WeakEntity, definition: DebugTaskDefinition, candidates: Arc<[Candidate]>, ) -> Self { Self { - workspace: workspace.downgrade(), + workspace, definition, candidates, selected_index: 0, @@ -55,7 +55,7 @@ pub struct AttachModal { impl AttachModal { pub fn new( definition: DebugTaskDefinition, - workspace: Entity, + workspace: WeakEntity, modal: bool, window: &mut Window, cx: &mut Context, @@ -82,7 +82,7 @@ impl AttachModal { } pub(super) fn with_processes( - workspace: Entity, + workspace: WeakEntity, definition: DebugTaskDefinition, processes: Arc<[Candidate]>, modal: bool, diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 113eb4c463..0670462e40 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -5,15 +5,15 @@ use crate::{ FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence, }; -use anyhow::Result; +use anyhow::{Result, anyhow}; use command_palette_hooks::CommandPaletteFilter; +use dap::StartDebuggingRequestArguments; use dap::adapters::DebugAdapterName; use dap::debugger_settings::DebugPanelDockPosition; use dap::{ ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent, client::SessionId, debugger_settings::DebuggerSettings, }; -use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; use gpui::{ Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity, @@ -54,12 +54,11 @@ pub enum DebugPanelEvent { } actions!(debug_panel, [ToggleFocus]); + pub struct DebugPanel { size: Pixels, sessions: Vec>, active_session: Option>, - /// This represents the last debug definition that was created in the new session modal - pub(crate) past_debug_definition: Option, project: Entity, workspace: WeakEntity, focus_handle: FocusHandle, @@ -80,7 +79,6 @@ impl DebugPanel { size: px(300.), sessions: vec![], active_session: None, - past_debug_definition: None, focus_handle: cx.focus_handle(), project, workspace: workspace.weak_handle(), @@ -992,6 +990,69 @@ impl DebugPanel { self.active_session = Some(session_item); cx.notify(); } + + pub(crate) fn save_scenario( + &self, + scenario: &DebugScenario, + worktree_id: WorktreeId, + window: &mut Window, + cx: &mut App, + ) -> Task> { + 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::>(&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 for DebugPanel {} diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 747c6d72a4..6306060c58 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -147,36 +147,7 @@ pub fn init(cx: &mut App) { }, ) .register_action(|workspace: &mut Workspace, _: &Start, window, cx| { - if let Some(debug_panel) = workspace.panel::(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() - } + NewSessionModal::show(workspace, window, cx); }); }) }) diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs index 1b528bea66..374d9de1ab 100644 --- a/crates/debugger_ui/src/new_session_modal.rs +++ b/crates/debugger_ui/src/new_session_modal.rs @@ -29,7 +29,7 @@ use ui::{ relative, rems, v_flex, }; use util::ResultExt; -use workspace::{ModalView, Workspace}; +use workspace::{ModalView, Workspace, pane}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; @@ -37,11 +37,12 @@ pub(super) struct NewSessionModal { workspace: WeakEntity, debug_panel: WeakEntity, mode: NewSessionMode, - stop_on_entry: ToggleState, - initialize_args: Option, + launch_picker: Entity>, + attach_mode: Entity, + custom_mode: Entity, debugger: Option, - last_selected_profile_name: Option, task_contexts: Arc, + _subscriptions: [Subscription; 2], } fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString { @@ -63,67 +64,126 @@ fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString { } impl NewSessionModal { - pub(super) fn new( - past_debug_definition: Option, - debug_panel: WeakEntity, - workspace: WeakEntity, - task_store: Option>, - task_contexts: TaskContexts, + pub(super) fn show( + workspace: &mut Workspace, window: &mut Window, - cx: &mut Context, - ) -> Self { - let debugger = past_debug_definition - .as_ref() - .map(|def| def.adapter.clone()); - - let stop_on_entry = past_debug_definition - .as_ref() - .and_then(|def| def.stop_on_entry); - - let launch_config = match past_debug_definition.map(|def| def.request) { - Some(DebugRequest::Launch(launch_config)) => Some(launch_config), - _ => None, + cx: &mut Context, + ) { + let Some(debug_panel) = workspace.panel::(cx) else { + return; }; + let task_store = workspace.project().read(cx).task_store().clone(); - if let Some(task_store) = task_store { - cx.defer_in(window, |this, window, cx| { - this.mode = NewSessionMode::scenario( - this.debug_panel.clone(), - this.workspace.clone(), - task_store, - window, - cx, - ); - }); - }; + cx.spawn_in(window, async move |workspace, cx| { + let task_contexts = Arc::from( + workspace + .update_in(cx, |workspace, window, cx| { + tasks_ui::task_contexts(workspace, window, cx) + })? + .await, + ); - Self { - workspace: workspace.clone(), - debugger, - debug_panel, - mode: NewSessionMode::launch(launch_config, window, cx), - stop_on_entry: stop_on_entry - .map(Into::into) - .unwrap_or(ToggleState::Unselected), - last_selected_profile_name: None, - initialize_args: None, - task_contexts: Arc::new(task_contexts), + workspace.update_in(cx, |workspace, window, cx| { + let workspace_handle = workspace.weak_handle(); + workspace.toggle_modal(window, cx, |window, cx| { + let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx); + + let launch_picker = cx.new(|cx| { + Picker::uniform_list( + DebugScenarioDelegate::new( + debug_panel.downgrade(), + workspace_handle.clone(), + task_store, + task_contexts.clone(), + ), + window, + cx, + ) + .modal(false) + }); + + let _subscriptions = [ + cx.subscribe(&launch_picker, |_, _, _, cx| { + cx.emit(DismissEvent); + }), + cx.subscribe( + &attach_mode.read(cx).attach_picker.clone(), + |_, _, _, cx| { + cx.emit(DismissEvent); + }, + ), + ]; + + let custom_mode = CustomMode::new(None, window, cx); + + Self { + launch_picker, + attach_mode, + custom_mode, + debugger: None, + mode: NewSessionMode::Launch, + debug_panel: debug_panel.downgrade(), + workspace: workspace_handle, + task_contexts, + _subscriptions, + } + }); + })?; + + anyhow::Ok(()) + }) + .detach(); + } + + fn render_mode(&self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { + let dap_menu = self.adapter_drop_down_menu(window, cx); + match self.mode { + NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| { + this.clone().render(window, cx).into_any_element() + }), + NewSessionMode::Custom => self.custom_mode.update(cx, |this, cx| { + this.clone().render(dap_menu, window, cx).into_any_element() + }), + NewSessionMode::Launch => v_flex() + .w(rems(34.)) + .child(self.launch_picker.clone()) + .into_any_element(), } } - fn debug_config(&self, cx: &App, debugger: &str) -> Option { - let request = self.mode.debug_task(cx)?; + fn mode_focus_handle(&self, cx: &App) -> FocusHandle { + match self.mode { + NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx), + NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx), + NewSessionMode::Launch => self.launch_picker.focus_handle(cx), + } + } + + fn debug_scenario(&self, debugger: &str, cx: &App) -> Option { + let request = match self.mode { + NewSessionMode::Custom => Some(DebugRequest::Launch( + self.custom_mode.read(cx).debug_request(cx), + )), + NewSessionMode::Attach => Some(DebugRequest::Attach( + self.attach_mode.read(cx).debug_request(), + )), + _ => None, + }?; let label = suggested_label(&request, debugger); + + let stop_on_entry = if let NewSessionMode::Custom = &self.mode { + Some(self.custom_mode.read(cx).stop_on_entry.selected()) + } else { + None + }; + Some(DebugScenario { adapter: debugger.to_owned().into(), label, request: Some(request), - initialize_args: self.initialize_args.clone(), + initialize_args: None, tcp_connection: None, - stop_on_entry: match self.stop_on_entry { - ToggleState::Selected => Some(true), - _ => None, - }, + stop_on_entry, build: None, }) } @@ -135,14 +195,14 @@ impl NewSessionModal { return; }; - if let NewSessionMode::Scenario(picker) = &self.mode { - picker.update(cx, |picker, cx| { + if let NewSessionMode::Launch = &self.mode { + self.launch_picker.update(cx, |picker, cx| { picker.delegate.confirm(false, window, cx); }); return; } - let Some(config) = self.debug_config(cx, debugger) else { + let Some(config) = self.debug_scenario(debugger, cx) else { log::error!("debug config not found in mode: {}", self.mode); return; }; @@ -189,7 +249,7 @@ impl NewSessionModal { &self, window: &mut Window, cx: &mut Context, - ) -> Option { + ) -> ui::DropdownMenu { let workspace = self.workspace.clone(); let weak = cx.weak_entity(); let label = self @@ -207,6 +267,7 @@ impl NewSessionModal { .and_then(|location| location.buffer.read(cx).language()) }) .cloned(); + DropdownMenu::new( "dap-adapter-picker", label, @@ -217,8 +278,8 @@ impl NewSessionModal { weak.update(cx, |this, cx| { this.debugger = Some(name.clone()); cx.notify(); - if let NewSessionMode::Attach(attach) = &this.mode { - Self::update_attach_picker(&attach, &name, window, cx); + if let NewSessionMode::Attach = &this.mode { + Self::update_attach_picker(&this.attach_mode, &name, window, cx); } }) .ok(); @@ -227,7 +288,6 @@ impl NewSessionModal { let mut available_adapters = workspace .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters()) - .ok() .unwrap_or_default(); if let Some(language) = active_buffer_language { available_adapters.sort_by_key(|adapter| { @@ -245,195 +305,24 @@ impl NewSessionModal { menu }), ) - .into() - } - - fn debug_config_drop_down_menu( - &self, - window: &mut Window, - cx: &mut Context, - ) -> ui::DropdownMenu { - let workspace = self.workspace.clone(); - let weak = cx.weak_entity(); - let last_profile = self.last_selected_profile_name.clone(); - let worktree = workspace - .update(cx, |this, cx| { - this.project().read(cx).visible_worktrees(cx).next() - }) - .unwrap_or_default(); - DropdownMenu::new( - "debug-config-menu", - last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()), - ContextMenu::build(window, cx, move |mut menu, _, cx| { - let setter_for_name = |task: DebugScenario| { - let weak = weak.clone(); - move |window: &mut Window, cx: &mut App| { - weak.update(cx, |this, cx| { - this.last_selected_profile_name = Some(SharedString::from(&task.label)); - this.debugger = Some(DebugAdapterName(task.adapter.clone())); - this.initialize_args = task.initialize_args.clone(); - match &task.request { - Some(DebugRequest::Launch(launch_config)) => { - this.mode = NewSessionMode::launch( - Some(launch_config.clone()), - window, - cx, - ); - } - Some(DebugRequest::Attach(_)) => { - let Some(workspace) = this.workspace.upgrade() else { - return; - }; - this.mode = NewSessionMode::attach( - this.debugger.clone(), - workspace, - window, - cx, - ); - this.mode.focus_handle(cx).focus(window); - if let Some((debugger, attach)) = - this.debugger.as_ref().zip(this.mode.as_attach()) - { - Self::update_attach_picker(&attach, &debugger, window, cx); - } - } - _ => log::warn!("Selected debug scenario without either attach or launch request specified"), - } - cx.notify(); - }) - .ok(); - } - }; - - let available_tasks: Vec = workspace - .update(cx, |this, cx| { - this.project() - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .iter() - .flat_map(|task_inventory| { - task_inventory.read(cx).list_debug_scenarios( - worktree - .as_ref() - .map(|worktree| worktree.read(cx).id()) - .iter() - .copied(), - ) - }) - .map(|(_source_kind, scenario)| scenario) - .collect() - }) - .ok() - .unwrap_or_default(); - - for debug_definition in available_tasks { - menu = menu.entry( - debug_definition.label.clone(), - None, - setter_for_name(debug_definition), - ); - } - menu - }), - ) } } static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger"); -static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile"); #[derive(Clone)] enum NewSessionMode { - Launch(Entity), - Scenario(Entity>), - Attach(Entity), -} - -impl NewSessionMode { - fn debug_task(&self, cx: &App) -> Option { - match self { - NewSessionMode::Launch(entity) => Some(entity.read(cx).debug_task(cx).into()), - NewSessionMode::Attach(entity) => Some(entity.read(cx).debug_task().into()), - NewSessionMode::Scenario(_) => None, - } - } - fn as_attach(&self) -> Option<&Entity> { - if let NewSessionMode::Attach(entity) = self { - Some(entity) - } else { - None - } - } - - fn scenario( - debug_panel: WeakEntity, - workspace: WeakEntity, - task_store: Entity, - window: &mut Window, - cx: &mut Context, - ) -> NewSessionMode { - let picker = cx.new(|cx| { - Picker::uniform_list( - DebugScenarioDelegate::new(debug_panel, workspace, task_store), - window, - cx, - ) - .modal(false) - }); - - cx.subscribe(&picker, |_, _, _, cx| { - cx.emit(DismissEvent); - }) - .detach(); - - picker.focus_handle(cx).focus(window); - NewSessionMode::Scenario(picker) - } - - fn attach( - debugger: Option, - workspace: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self::Attach(AttachMode::new(debugger, workspace, window, cx)) - } - - fn launch( - past_launch_config: Option, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self::Launch(LaunchMode::new(past_launch_config, window, cx)) - } - - fn has_match(&self, cx: &App) -> bool { - match self { - NewSessionMode::Scenario(picker) => picker.read(cx).delegate.match_count() > 0, - NewSessionMode::Attach(picker) => { - picker - .read(cx) - .attach_picker - .read(cx) - .picker - .read(cx) - .delegate - .match_count() - > 0 - } - _ => false, - } - } + Custom, + Attach, + Launch, } impl std::fmt::Display for NewSessionMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mode = match self { - NewSessionMode::Launch(_) => "launch".to_owned(), - NewSessionMode::Attach(_) => "attach".to_owned(), - NewSessionMode::Scenario(_) => "scenario picker".to_owned(), + NewSessionMode::Launch => "Launch".to_owned(), + NewSessionMode::Attach => "Attach".to_owned(), + NewSessionMode::Custom => "Custom".to_owned(), }; write!(f, "{}", mode) @@ -442,28 +331,7 @@ impl std::fmt::Display for NewSessionMode { impl Focusable for NewSessionMode { fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self { - NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx), - NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx), - NewSessionMode::Scenario(entity) => entity.read(cx).focus_handle(cx), - } - } -} - -impl RenderOnce for NewSessionMode { - fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement { - match self { - NewSessionMode::Launch(entity) => entity.update(cx, |this, cx| { - this.clone().render(window, cx).into_any_element() - }), - NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| { - this.clone().render(window, cx).into_any_element() - }), - NewSessionMode::Scenario(entity) => v_flex() - .w(rems(34.)) - .child(entity.clone()) - .into_any_element(), - } + cx.focus_handle() } } @@ -514,11 +382,36 @@ impl Render for NewSessionModal { v_flex() .size_full() .w(rems(34.)) + .key_context("Pane") .elevation_3(cx) .bg(cx.theme().colors().elevated_surface_background) .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| { cx.emit(DismissEvent); })) + .on_action( + cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| { + this.mode = match this.mode { + NewSessionMode::Attach => NewSessionMode::Launch, + NewSessionMode::Launch => NewSessionMode::Attach, + _ => { + return; + } + }; + + this.mode_focus_handle(cx).focus(window); + }), + ) + .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| { + this.mode = match this.mode { + NewSessionMode::Attach => NewSessionMode::Launch, + NewSessionMode::Launch => NewSessionMode::Attach, + _ => { + return; + } + }; + + this.mode_focus_handle(cx).focus(window); + })) .child( h_flex() .w_full() @@ -529,84 +422,44 @@ impl Render for NewSessionModal { .justify_start() .w_full() .child( - ToggleButton::new("debugger-session-ui-picker-button", "Scenarios") + ToggleButton::new("debugger-session-ui-picker-button", "Launch") .size(ButtonSize::Default) .style(ui::ButtonStyle::Subtle) - .toggle_state(matches!(self.mode, NewSessionMode::Scenario(_))) + .toggle_state(matches!(self.mode, NewSessionMode::Launch)) .on_click(cx.listener(|this, _, window, cx| { - let Some(task_store) = this - .workspace - .update(cx, |workspace, cx| { - workspace.project().read(cx).task_store().clone() - }) - .ok() - else { - return; - }; - - this.mode = NewSessionMode::scenario( - this.debug_panel.clone(), - this.workspace.clone(), - task_store, - window, - cx, - ); - + this.mode = NewSessionMode::Launch; + this.mode_focus_handle(cx).focus(window); cx.notify(); })) .first(), ) .child( - ToggleButton::new( - "debugger-session-ui-launch-button", - "New Session", - ) - .size(ButtonSize::Default) - .style(ui::ButtonStyle::Subtle) - .toggle_state(matches!(self.mode, NewSessionMode::Launch(_))) - .on_click(cx.listener(|this, _, window, cx| { - this.mode = NewSessionMode::launch(None, window, cx); - this.mode.focus_handle(cx).focus(window); - cx.notify(); - })) - .middle(), - ) - .child( - ToggleButton::new( - "debugger-session-ui-attach-button", - "Attach to Process", - ) - .size(ButtonSize::Default) - .toggle_state(matches!(self.mode, NewSessionMode::Attach(_))) - .style(ui::ButtonStyle::Subtle) - .on_click(cx.listener(|this, _, window, cx| { - let Some(workspace) = this.workspace.upgrade() else { - return; - }; - this.mode = NewSessionMode::attach( - this.debugger.clone(), - workspace, - window, - cx, - ); - this.mode.focus_handle(cx).focus(window); - if let Some((debugger, attach)) = - this.debugger.as_ref().zip(this.mode.as_attach()) - { - Self::update_attach_picker(&attach, &debugger, window, cx); - } + ToggleButton::new("debugger-session-ui-attach-button", "Attach") + .size(ButtonSize::Default) + .toggle_state(matches!(self.mode, NewSessionMode::Attach)) + .style(ui::ButtonStyle::Subtle) + .on_click(cx.listener(|this, _, window, cx| { + this.mode = NewSessionMode::Attach; - cx.notify(); - })) - .last(), + if let Some(debugger) = this.debugger.as_ref() { + Self::update_attach_picker( + &this.attach_mode, + &debugger, + window, + cx, + ); + } + this.mode_focus_handle(cx).focus(window); + cx.notify(); + })) + .last(), ), ) .justify_between() - .children(self.adapter_drop_down_menu(window, cx)) .border_color(cx.theme().colors().border_variant) .border_b_1(), ) - .child(v_flex().child(self.mode.clone().render(window, cx))) + .child(v_flex().child(self.render_mode(window, cx))) .child( h_flex() .justify_between() @@ -615,53 +468,91 @@ impl Render for NewSessionModal { .border_color(cx.theme().colors().border_variant) .border_t_1() .w_full() - .child( - matches!(self.mode, NewSessionMode::Scenario(_)) - .not() - .then(|| { - self.debug_config_drop_down_menu(window, cx) - .into_any_element() - }) - .unwrap_or_else(|| v_flex().w_full().into_any_element()), - ) - .child( - h_flex() - .justify_end() - .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| { - let weak = cx.weak_entity(); - this.child( - CheckboxWithLabel::new( - "debugger-stop-on-entry", - Label::new("Stop on Entry").size(ui::LabelSize::Small), - self.stop_on_entry, - move |state, _, cx| { - weak.update(cx, |this, _| { - this.stop_on_entry = *state; - }) - .ok(); - }, - ) - .checkbox_position(ui::IconPosition::End), - ) - }) - .child( - Button::new("debugger-spawn", "Start") - .on_click(cx.listener(|this, _, window, cx| match &this.mode { - NewSessionMode::Scenario(picker) => { - picker.update(cx, |picker, cx| { - picker.delegate.confirm(true, window, cx) - }) + .child(match self.mode { + NewSessionMode::Attach => { + div().child(self.adapter_drop_down_menu(window, cx)) + } + NewSessionMode::Launch => div().child( + Button::new("new-session-modal-custom", "Custom").on_click({ + let this = cx.weak_entity(); + move |_, window, cx| { + this.update(cx, |this, cx| { + this.mode = NewSessionMode::Custom; + this.mode_focus_handle(cx).focus(window); + }) + .ok(); + } + }), + ), + NewSessionMode::Custom => div().child( + Button::new("new-session-modal-back", "Save to .zed/debug.json...") + .on_click(cx.listener(|this, _, window, cx| { + let Some(save_scenario_task) = this + .debugger + .as_ref() + .and_then(|debugger| this.debug_scenario(&debugger, cx)) + .zip(this.task_contexts.worktree()) + .and_then(|(scenario, worktree_id)| { + this.debug_panel + .update(cx, |panel, cx| { + panel.save_scenario( + &scenario, + worktree_id, + window, + cx, + ) + }) + .ok() + }) + else { + return; + }; + + cx.spawn(async move |this, cx| { + if save_scenario_task.await.is_ok() { + this.update(cx, |_, cx| cx.emit(DismissEvent)).ok(); } - _ => this.start_new_session(window, cx), - })) - .disabled(match self.mode { - NewSessionMode::Scenario(_) => !self.mode.has_match(cx), - NewSessionMode::Attach(_) => { - self.debugger.is_none() || !self.mode.has_match(cx) - } - NewSessionMode::Launch(_) => self.debugger.is_none(), - }), - ), + }) + .detach(); + })) + .disabled( + self.debugger.is_none() + || self.custom_mode.read(cx).program.read(cx).is_empty(cx), + ), + ), + }) + .child( + Button::new("debugger-spawn", "Start") + .on_click(cx.listener(|this, _, window, cx| match &this.mode { + NewSessionMode::Launch => { + this.launch_picker.update(cx, |picker, cx| { + picker.delegate.confirm(true, window, cx) + }) + } + _ => this.start_new_session(window, cx), + })) + .disabled(match self.mode { + NewSessionMode::Launch => { + !self.launch_picker.read(cx).delegate.matches.is_empty() + } + NewSessionMode::Attach => { + self.debugger.is_none() + || self + .attach_mode + .read(cx) + .attach_picker + .read(cx) + .picker + .read(cx) + .delegate + .match_count() + == 0 + } + NewSessionMode::Custom => { + self.debugger.is_none() + || self.custom_mode.read(cx).program.read(cx).is_empty(cx) + } + }), ), ) } @@ -670,38 +561,12 @@ impl Render for NewSessionModal { impl EventEmitter for NewSessionModal {} impl Focusable for NewSessionModal { fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle { - self.mode.focus_handle(cx) + self.mode_focus_handle(cx) } } impl ModalView for NewSessionModal {} -impl RenderOnce for LaunchMode { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .p_2() - .w_full() - .gap_3() - .track_focus(&self.program.focus_handle(cx)) - .child( - div().child( - Label::new("Program") - .size(ui::LabelSize::Small) - .color(Color::Muted), - ), - ) - .child(render_editor(&self.program, window, cx)) - .child( - div().child( - Label::new("Working Directory") - .size(ui::LabelSize::Small) - .color(Color::Muted), - ), - ) - .child(render_editor(&self.cwd, window, cx)) - } -} - impl RenderOnce for AttachMode { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { v_flex() @@ -711,15 +576,14 @@ impl RenderOnce for AttachMode { } } -use std::rc::Rc; - #[derive(Clone)] -pub(super) struct LaunchMode { +pub(super) struct CustomMode { program: Entity, cwd: Entity, + stop_on_entry: ToggleState, } -impl LaunchMode { +impl CustomMode { pub(super) fn new( past_launch_config: Option, window: &mut Window, @@ -744,10 +608,14 @@ impl LaunchMode { this.set_text(past_cwd.to_string_lossy(), window, cx); }; }); - cx.new(|_| Self { program, cwd }) + cx.new(|_| Self { + program, + cwd, + stop_on_entry: ToggleState::Unselected, + }) } - pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest { + pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest { let path = self.cwd.read(cx).text(cx); task::LaunchRequest { program: self.program.read(cx).text(cx), @@ -756,19 +624,66 @@ impl LaunchMode { env: Default::default(), } } + + fn render( + &mut self, + adapter_menu: DropdownMenu, + window: &mut Window, + cx: &mut ui::Context, + ) -> impl IntoElement { + v_flex() + .p_2() + .w_full() + .gap_3() + .track_focus(&self.program.focus_handle(cx)) + .child( + div().child( + Label::new("Program") + .size(ui::LabelSize::Small) + .color(Color::Muted), + ), + ) + .child(render_editor(&self.program, window, cx)) + .child( + h_flex() + .child( + Label::new("Debugger") + .size(ui::LabelSize::Small) + .color(Color::Muted), + ) + .gap(ui::DynamicSpacing::Base08.rems(cx)) + .child(adapter_menu), + ) + .child( + CheckboxWithLabel::new( + "debugger-stop-on-entry", + Label::new("Stop on Entry").size(ui::LabelSize::Small), + self.stop_on_entry, + { + let this = cx.weak_entity(); + move |state, _, cx| { + this.update(cx, |this, _| { + this.stop_on_entry = *state; + }) + .ok(); + } + }, + ) + .checkbox_position(ui::IconPosition::End), + ) + } } #[derive(Clone)] pub(super) struct AttachMode { pub(super) definition: DebugTaskDefinition, pub(super) attach_picker: Entity, - _subscription: Rc, } impl AttachMode { pub(super) fn new( debugger: Option, - workspace: Entity, + workspace: WeakEntity, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -787,17 +702,12 @@ impl AttachMode { modal }); - let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| { - cx.emit(DismissEvent); - }); - cx.new(|_| Self { definition, attach_picker, - _subscription: Rc::new(subscription), }) } - pub(super) fn debug_task(&self) -> task::AttachRequest { + pub(super) fn debug_request(&self) -> task::AttachRequest { task::AttachRequest { process_id: None } } } @@ -810,6 +720,7 @@ pub(super) struct DebugScenarioDelegate { prompt: String, debug_panel: WeakEntity, workspace: WeakEntity, + task_contexts: Arc, } impl DebugScenarioDelegate { @@ -817,6 +728,7 @@ impl DebugScenarioDelegate { debug_panel: WeakEntity, workspace: WeakEntity, task_store: Entity, + task_contexts: Arc, ) -> Self { Self { task_store, @@ -826,6 +738,7 @@ impl DebugScenarioDelegate { prompt: String::new(), debug_panel, workspace, + task_contexts, } } } @@ -860,45 +773,55 @@ impl PickerDelegate for DebugScenarioDelegate { window: &mut Window, cx: &mut Context>, ) -> gpui::Task<()> { - let candidates: Vec<_> = match &self.candidates { - Some(candidates) => candidates - .into_iter() - .enumerate() - .map(|(index, (_, candidate))| { - StringMatchCandidate::new(index, candidate.label.as_ref()) - }) - .collect(), - None => { - let worktree_ids: Vec<_> = self - .workspace - .update(cx, |this, cx| { - this.visible_worktrees(cx) - .map(|tree| tree.read(cx).id()) - .collect() - }) - .ok() - .unwrap_or_default(); + let candidates = self.candidates.clone(); + let workspace = self.workspace.clone(); + let task_store = self.task_store.clone(); - let scenarios: Vec<_> = self - .task_store - .read(cx) - .task_inventory() - .map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter())) - .unwrap_or_default(); - - self.candidates = Some(scenarios.clone()); - - scenarios + cx.spawn_in(window, async move |picker, cx| { + let candidates: Vec<_> = match &candidates { + Some(candidates) => candidates .into_iter() .enumerate() .map(|(index, (_, candidate))| { StringMatchCandidate::new(index, candidate.label.as_ref()) }) - .collect() - } - }; + .collect(), + None => { + let worktree_ids: Vec<_> = workspace + .update(cx, |this, cx| { + this.visible_worktrees(cx) + .map(|tree| tree.read(cx).id()) + .collect() + }) + .ok() + .unwrap_or_default(); + + let scenarios: Vec<_> = task_store + .update(cx, |task_store, cx| { + task_store.task_inventory().map(|item| { + item.read(cx).list_debug_scenarios(worktree_ids.into_iter()) + }) + }) + .ok() + .flatten() + .unwrap_or_default(); + + picker + .update(cx, |picker, _| { + picker.delegate.candidates = Some(scenarios.clone()); + }) + .ok(); + + scenarios + .into_iter() + .enumerate() + .map(|(index, (_, candidate))| { + StringMatchCandidate::new(index, candidate.label.as_ref()) + }) + .collect() + } + }; - cx.spawn_in(window, async move |picker, cx| { let matches = fuzzy::match_strings( &candidates, &query, @@ -941,52 +864,28 @@ impl PickerDelegate for DebugScenarioDelegate { return; }; - let task_context = if let TaskSourceKind::Worktree { + let (task_context, worktree_id) = if let TaskSourceKind::Worktree { id: worktree_id, directory_in_worktree: _, id_base: _, } = task_source_kind { - let workspace = self.workspace.clone(); - - cx.spawn_in(window, async move |_, cx| { - workspace - .update_in(cx, |workspace, window, cx| { - tasks_ui::task_contexts(workspace, window, cx) - }) - .ok()? - .await - .task_context_for_worktree_id(worktree_id) - .cloned() - .map(|context| (context, Some(worktree_id))) - }) + self.task_contexts + .task_context_for_worktree_id(worktree_id) + .cloned() + .map(|context| (context, Some(worktree_id))) } else { - gpui::Task::ready(None) - }; + None + } + .unwrap_or_default(); - cx.spawn_in(window, async move |this, cx| { - let (task_context, worktree_id) = task_context.await.unwrap_or_default(); - - this.update_in(cx, |this, window, cx| { - this.delegate - .debug_panel - .update(cx, |panel, cx| { - panel.start_session( - debug_scenario, - task_context, - None, - worktree_id, - window, - cx, - ); - }) - .ok(); - - cx.emit(DismissEvent); + self.debug_panel + .update(cx, |panel, cx| { + panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx); }) .ok(); - }) - .detach(); + + cx.emit(DismissEvent); } fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index bc69308861..4264568713 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -864,7 +864,7 @@ impl RunningState { 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 { label, diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs index 4a0acea346..b99d1d36c4 100644 --- a/crates/debugger_ui/src/tests/attach_modal.rs +++ b/crates/debugger_ui/src/tests/attach_modal.rs @@ -103,7 +103,7 @@ async fn test_show_attach_modal_and_select_process( }); let attach_modal = workspace .update(cx, |workspace, window, cx| { - let workspace_handle = cx.entity(); + let workspace_handle = cx.weak_entity(); workspace.toggle_modal(window, cx, |window, cx| { AttachModal::with_processes( workspace_handle, diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 3b28ff87f6..cd8e360236 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -141,7 +141,7 @@ impl JsonLspAdapter { }, { "fileMatch": [ - schema_file_match(paths::debug_tasks_file()), + schema_file_match(paths::debug_scenarios_file()), paths::local_debug_file_relative_path() ], "schema": debug_schema, diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs index 83bb7e14a0..c0e506fcd1 100644 --- a/crates/paths/src/paths.rs +++ b/crates/paths/src/paths.rs @@ -216,9 +216,9 @@ pub fn tasks_file() -> &'static PathBuf { } /// Returns the path to the `debug.json` file. -pub fn debug_tasks_file() -> &'static PathBuf { - static DEBUG_TASKS_FILE: OnceLock = OnceLock::new(); - DEBUG_TASKS_FILE.get_or_init(|| config_dir().join("debug.json")) +pub fn debug_scenarios_file() -> &'static PathBuf { + static DEBUG_SCENARIOS_FILE: OnceLock = OnceLock::new(); + DEBUG_SCENARIOS_FILE.get_or_init(|| config_dir().join("debug.json")) } /// Returns the path to the extensions directory. diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index c5f62f36af..eff14a0306 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -193,22 +193,22 @@ pub struct DebugScenario { /// Name of the debug task pub label: SharedString, /// A task to run prior to spawning the debuggee. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub build: Option, #[serde(flatten)] pub request: Option, /// Additional initialization arguments to be sent on DAP initialization - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub initialize_args: Option, /// Optional TCP connection information /// /// 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 /// that is already running or is started by another process. - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub tcp_connection: Option, /// 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, } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 410b8ff204..dbb5d0c1ea 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -701,7 +701,7 @@ fn register_actions( }) .register_action(move |_: &mut Workspace, _: &OpenDebugTasks, window, cx| { open_settings_file( - paths::debug_tasks_file(), + paths::debug_scenarios_file(), || settings::initial_debug_tasks_content().as_ref().into(), window, cx,