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 9e83046c42184821a82167439db64b25b27dfd37..bf6bc016d6dafb83c6de4fdd9db711397f027163 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 295d18e4b3b8c217bad19b4d676897f47a18f905..dfc15009910bc9404692934c25320f02d453d420 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 e054985f92e6b205f38d06735fd0b748da650299..9575ff546d86fec5dea8366643c534bfd1f40ccb 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 113eb4c463585ac6950c066c39c3159381790c45..0670462e4051a216e4b36e7f86305e314bfcfb4d 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 747c6d72a47a5b04f7c4284b5b04bf03fe582d6d..6306060c5891e4e992b00c9debcacc389609cf00 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 1b528bea66da24b8716d27d72fa95b5db1f0f9c9..374d9de1ab48a0e148be1d01e2ab64c823ae3a3c 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()); + cx: &mut Context, + ) { + let Some(debug_panel) = workspace.panel::(cx) else { + return; + }; + let task_store = workspace.project().read(cx).task_store().clone(); - let stop_on_entry = past_debug_definition - .as_ref() - .and_then(|def| def.stop_on_entry); + 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, + ); + + 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 launch_config = match past_debug_definition.map(|def| def.request) { - Some(DebugRequest::Launch(launch_config)) => Some(launch_config), - _ => None, - }; + 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, + } + }); + })?; - 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, - ); - }); - }; + anyhow::Ok(()) + }) + .detach(); + } - 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), + 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 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_config(&self, cx: &App, debugger: &str) -> Option { - let request = self.mode.debug_task(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); - } - - cx.notify(); - })) - .last(), + 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; + + 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) - }) - } - _ => 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) + .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(); } - 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 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()); + let candidates = self.candidates.clone(); + let workspace = self.workspace.clone(); + let task_store = self.task_store.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) - }; - - 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(); + None + } + .unwrap_or_default(); - 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 bc69308861bde47fdd5e5eb70c28525f6c19afc6..4264568713a1f94e0ad332690eabb5d43eb8b800 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 4a0acea3468e4780defe5a9e07e5a75ae548cfb5..b99d1d36c4609a23f156debc0fd3043120b2a980 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 3b28ff87f6c5ce1b225e3f6bcc8230eaedb7283b..cd8e360236886d7438a690c0646017947164c507 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 83bb7e14a0213b58cff1e22504d53d7a567cc3cf..c0e506fcd1d5c3670ae6d26bd00824eab10e257c 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 c5f62f36af9b89ddc949c825696022b520f3f953..eff14a030686cbbf536e15d47b3c382cf770f80e 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 410b8ff204f1bf4e82547d66dcf15b8d4ecfb60f..dbb5d0c1ea019a943a58450383fe148d88201568 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,