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,