From 1445af559b22ceec87bbad419ecbb4e07e6d2c07 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 29 May 2025 21:33:52 -0400 Subject: [PATCH] Unify the tasks modal and the new session modal (#31646) Release Notes: - Debugger Beta: added a button to the quick action bar to start a debug session or spawn a task, depending on which of these actions was taken most recently. - Debugger Beta: incorporated the tasks modal into the new session modal as an additional tab. --------- Co-authored-by: Julia Ryan Co-authored-by: Julia Ryan Co-authored-by: Anthony Eid Co-authored-by: Mikayla --- assets/keymaps/default-linux.json | 7 + assets/keymaps/default-macos.json | 8 + crates/debugger_ui/src/debugger_panel.rs | 31 +- crates/debugger_ui/src/debugger_ui.rs | 51 ++- crates/debugger_ui/src/new_session_modal.rs | 429 ++++++++++-------- .../src/tests/new_session_modal.rs | 13 +- crates/gpui/src/key_dispatch.rs | 2 +- crates/tasks_ui/src/modal.rs | 33 +- crates/tasks_ui/src/tasks_ui.rs | 15 +- crates/workspace/src/tasks.rs | 4 + crates/workspace/src/workspace.rs | 24 +- crates/zed/src/zed/quick_action_bar.rs | 41 ++ 12 files changed, 434 insertions(+), 224 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 58fda9dc4d..73d49292c5 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1019,5 +1019,12 @@ "bindings": { "enter": "menu::Confirm" } + }, + { + "context": "RunModal", + "bindings": { + "ctrl-tab": "pane::ActivateNextItem", + "ctrl-shift-tab": "pane::ActivatePreviousItem" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 05642de920..8b86268e98 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1109,5 +1109,13 @@ "bindings": { "enter": "menu::Confirm" } + }, + { + "context": "RunModal", + "use_key_equivalents": true, + "bindings": { + "ctrl-tab": "pane::ActivateNextItem", + "ctrl-shift-tab": "pane::ActivatePreviousItem" + } } ] diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b72d97501f..9374fc7282 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -5,7 +5,7 @@ use crate::{ ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, - ToggleSessionPicker, ToggleThreadPicker, persistence, + ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal, }; use anyhow::{Context as _, Result, anyhow}; use command_palette_hooks::CommandPaletteFilter; @@ -65,6 +65,7 @@ pub struct DebugPanel { workspace: WeakEntity, focus_handle: FocusHandle, context_menu: Option<(Entity, Point, Subscription)>, + debug_scenario_scheduled_last: bool, pub(crate) thread_picker_menu_handle: PopoverMenuHandle, pub(crate) session_picker_menu_handle: PopoverMenuHandle, fs: Arc, @@ -103,6 +104,7 @@ impl DebugPanel { thread_picker_menu_handle, session_picker_menu_handle, _subscriptions: [focus_subscription], + debug_scenario_scheduled_last: true, } }) } @@ -264,6 +266,7 @@ impl DebugPanel { cx, ) }); + self.debug_scenario_scheduled_last = true; if let Some(inventory) = self .project .read(cx) @@ -1381,4 +1384,30 @@ impl workspace::DebuggerProvider for DebuggerProvider { }) }) } + + fn spawn_task_or_modal( + &self, + workspace: &mut Workspace, + action: &tasks_ui::Spawn, + window: &mut Window, + cx: &mut Context, + ) { + spawn_task_or_modal(workspace, action, window, cx); + } + + fn debug_scenario_scheduled(&self, cx: &mut App) { + self.0.update(cx, |this, _| { + this.debug_scenario_scheduled_last = true; + }); + } + + fn task_scheduled(&self, cx: &mut App) { + self.0.update(cx, |this, _| { + this.debug_scenario_scheduled_last = false; + }) + } + + fn debug_scenario_scheduled_last(&self, cx: &App) -> bool { + self.0.read(cx).debug_scenario_scheduled_last + } } diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 3676cec27f..43c7678797 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -3,11 +3,12 @@ use debugger_panel::{DebugPanel, ToggleFocus}; use editor::Editor; use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt}; use gpui::{App, EntityInputHandler, actions}; -use new_session_modal::NewSessionModal; +use new_session_modal::{NewSessionModal, NewSessionMode}; use project::debugger::{self, breakpoint_store::SourceBreakpoint}; use session::DebugSession; use settings::Settings; use stack_trace_view::StackTraceView; +use tasks_ui::{Spawn, TaskOverrides}; use util::maybe; use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace}; @@ -62,6 +63,7 @@ pub fn init(cx: &mut App) { cx.when_flag_enabled::(window, |workspace, _, _| { workspace + .register_action(spawn_task_or_modal) .register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::(window, cx); }) @@ -208,7 +210,7 @@ pub fn init(cx: &mut App) { }, ) .register_action(|workspace: &mut Workspace, _: &Start, window, cx| { - NewSessionModal::show(workspace, window, cx); + NewSessionModal::show(workspace, window, NewSessionMode::Launch, None, cx); }) .register_action( |workspace: &mut Workspace, _: &RerunLastSession, window, cx| { @@ -309,3 +311,48 @@ pub fn init(cx: &mut App) { }) .detach(); } + +fn spawn_task_or_modal( + workspace: &mut Workspace, + action: &Spawn, + window: &mut ui::Window, + cx: &mut ui::Context, +) { + match action { + Spawn::ByName { + task_name, + reveal_target, + } => { + let overrides = reveal_target.map(|reveal_target| TaskOverrides { + reveal_target: Some(reveal_target), + }); + let name = task_name.clone(); + tasks_ui::spawn_tasks_filtered( + move |(_, task)| task.label.eq(&name), + overrides, + window, + cx, + ) + .detach_and_log_err(cx) + } + Spawn::ByTag { + task_tag, + reveal_target, + } => { + let overrides = reveal_target.map(|reveal_target| TaskOverrides { + reveal_target: Some(reveal_target), + }); + let tag = task_tag.clone(); + tasks_ui::spawn_tasks_filtered( + move |(_, task)| task.tags.contains(&tag), + overrides, + window, + cx, + ) + .detach_and_log_err(cx) + } + Spawn::ViaModal { reveal_target } => { + NewSessionModal::show(workspace, window, NewSessionMode::Task, *reveal_target, cx); + } + } +} diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs index b27af9f876..aeac24d3d1 100644 --- a/crates/debugger_ui/src/new_session_modal.rs +++ b/crates/debugger_ui/src/new_session_modal.rs @@ -8,6 +8,7 @@ use std::{ time::Duration, usize, }; +use tasks_ui::{TaskOverrides, TasksModal}; use dap::{ DapRegistry, DebugRequest, TelemetrySpawnLocation, adapters::DebugAdapterName, send_telemetry, @@ -16,12 +17,12 @@ use editor::{Anchor, Editor, EditorElement, EditorStyle, scroll::Autoscroll}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Animation, AnimationExt as _, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage, + Focusable, KeyContext, Render, Subscription, TextStyle, Transformation, WeakEntity, percentage, }; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use project::{ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore}; use settings::Settings; -use task::{DebugScenario, LaunchRequest, ZedDebugConfig}; +use task::{DebugScenario, LaunchRequest, RevealTarget, ZedDebugConfig}; use theme::ThemeSettings; use ui::{ ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, @@ -47,10 +48,11 @@ pub(super) struct NewSessionModal { mode: NewSessionMode, launch_picker: Entity>, attach_mode: Entity, - custom_mode: Entity, + configure_mode: Entity, + task_mode: TaskMode, debugger: Option, save_scenario_state: Option, - _subscriptions: [Subscription; 2], + _subscriptions: [Subscription; 3], } fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString { @@ -75,6 +77,8 @@ impl NewSessionModal { pub(super) fn show( workspace: &mut Workspace, window: &mut Window, + mode: NewSessionMode, + reveal_target: Option, cx: &mut Context, ) { let Some(debug_panel) = workspace.panel::(cx) else { @@ -84,20 +88,50 @@ impl NewSessionModal { let languages = workspace.app_state().languages.clone(); cx.spawn_in(window, async move |workspace, cx| { + let task_contexts = workspace + .update_in(cx, |workspace, window, cx| { + tasks_ui::task_contexts(workspace, window, cx) + })? + .await; + let 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(), task_store), - window, - cx, - ) - .modal(false) + let mut delegate = + DebugScenarioDelegate::new(debug_panel.downgrade(), task_store.clone()); + delegate.task_contexts_loaded(task_contexts.clone(), languages, window, cx); + Picker::uniform_list(delegate, window, cx).modal(false) }); + let configure_mode = ConfigureMode::new(None, window, cx); + if let Some(active_cwd) = task_contexts + .active_context() + .and_then(|context| context.cwd.clone()) + { + configure_mode.update(cx, |configure_mode, cx| { + configure_mode.load(active_cwd, window, cx); + }); + } + + let task_overrides = Some(TaskOverrides { reveal_target }); + + let task_mode = TaskMode { + task_modal: cx.new(|cx| { + TasksModal::new( + task_store.clone(), + task_contexts, + task_overrides, + false, + workspace_handle.clone(), + window, + cx, + ) + }), + }; + let _subscriptions = [ cx.subscribe(&launch_picker, |_, _, _, cx| { cx.emit(DismissEvent); @@ -108,52 +142,18 @@ impl NewSessionModal { cx.emit(DismissEvent); }, ), + cx.subscribe(&task_mode.task_modal, |_, _, _: &DismissEvent, cx| { + cx.emit(DismissEvent) + }), ]; - let custom_mode = CustomMode::new(None, window, cx); - - cx.spawn_in(window, { - let workspace_handle = workspace_handle.clone(); - async move |this, cx| { - let task_contexts = workspace_handle - .update_in(cx, |workspace, window, cx| { - tasks_ui::task_contexts(workspace, window, cx) - })? - .await; - - this.update_in(cx, |this, window, cx| { - if let Some(active_cwd) = task_contexts - .active_context() - .and_then(|context| context.cwd.clone()) - { - this.custom_mode.update(cx, |custom, cx| { - custom.load(active_cwd, window, cx); - }); - - this.debugger = None; - } - - this.launch_picker.update(cx, |picker, cx| { - picker.delegate.task_contexts_loaded( - task_contexts, - languages, - window, - cx, - ); - picker.refresh(window, cx); - cx.notify(); - }); - }) - } - }) - .detach(); - Self { launch_picker, attach_mode, - custom_mode, + configure_mode, + task_mode, debugger: None, - mode: NewSessionMode::Launch, + mode, debug_panel: debug_panel.downgrade(), workspace: workspace_handle, save_scenario_state: None, @@ -170,10 +170,17 @@ impl NewSessionModal { fn render_mode(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { let dap_menu = self.adapter_drop_down_menu(window, cx); match self.mode { + NewSessionMode::Task => self + .task_mode + .task_modal + .read(cx) + .picker + .clone() + .into_any_element(), 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| { + NewSessionMode::Configure => self.configure_mode.update(cx, |this, cx| { this.clone().render(dap_menu, window, cx).into_any_element() }), NewSessionMode::Launch => v_flex() @@ -185,16 +192,17 @@ impl NewSessionModal { fn mode_focus_handle(&self, cx: &App) -> FocusHandle { match self.mode { + NewSessionMode::Task => self.task_mode.task_modal.focus_handle(cx), NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx), - NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx), + NewSessionMode::Configure => self.configure_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::Configure => Some(DebugRequest::Launch( + self.configure_mode.read(cx).debug_request(cx), )), NewSessionMode::Attach => Some(DebugRequest::Attach( self.attach_mode.read(cx).debug_request(), @@ -203,8 +211,8 @@ impl NewSessionModal { }?; 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()) + let stop_on_entry = if let NewSessionMode::Configure = &self.mode { + Some(self.configure_mode.read(cx).stop_on_entry.selected()) } else { None }; @@ -527,7 +535,8 @@ static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select De #[derive(Clone)] pub(crate) enum NewSessionMode { - Custom, + Task, + Configure, Attach, Launch, } @@ -535,9 +544,10 @@ pub(crate) enum NewSessionMode { 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::Custom => "Custom".to_owned(), + NewSessionMode::Task => "Run", + NewSessionMode::Launch => "Debug", + NewSessionMode::Attach => "Attach", + NewSessionMode::Configure => "Configure Debugger", }; write!(f, "{}", mode) @@ -597,36 +607,39 @@ impl Render for NewSessionModal { v_flex() .size_full() .w(rems(34.)) - .key_context("Pane") + .key_context({ + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("Pane"); + key_context.add("RunModal"); + key_context + }) .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::ActivateNextItem, window, cx| { + this.mode = match this.mode { + NewSessionMode::Task => NewSessionMode::Launch, + NewSessionMode::Launch => NewSessionMode::Attach, + NewSessionMode::Attach => NewSessionMode::Configure, + NewSessionMode::Configure => NewSessionMode::Task, + }; + + this.mode_focus_handle(cx).focus(window); + })) .on_action( cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| { this.mode = match this.mode { + NewSessionMode::Task => NewSessionMode::Configure, + NewSessionMode::Launch => NewSessionMode::Task, NewSessionMode::Attach => NewSessionMode::Launch, - NewSessionMode::Launch => NewSessionMode::Attach, - _ => { - return; - } + NewSessionMode::Configure => NewSessionMode::Attach, }; 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() @@ -637,37 +650,73 @@ impl Render for NewSessionModal { .justify_start() .w_full() .child( - ToggleButton::new("debugger-session-ui-picker-button", "Launch") - .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; - this.mode_focus_handle(cx).focus(window); - cx.notify(); - })) - .first(), + ToggleButton::new( + "debugger-session-ui-tasks-button", + NewSessionMode::Task.to_string(), + ) + .size(ButtonSize::Default) + .toggle_state(matches!(self.mode, NewSessionMode::Task)) + .style(ui::ButtonStyle::Subtle) + .on_click(cx.listener(|this, _, window, cx| { + this.mode = NewSessionMode::Task; + this.mode_focus_handle(cx).focus(window); + cx.notify(); + })) + .first(), ) .child( - 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; + ToggleButton::new( + "debugger-session-ui-launch-button", + NewSessionMode::Launch.to_string(), + ) + .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; + this.mode_focus_handle(cx).focus(window); + cx.notify(); + })) + .middle(), + ) + .child( + ToggleButton::new( + "debugger-session-ui-attach-button", + NewSessionMode::Attach.to_string(), + ) + .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(), + 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(); + })) + .middle(), + ) + .child( + ToggleButton::new( + "debugger-session-ui-custom-button", + NewSessionMode::Configure.to_string(), + ) + .size(ButtonSize::Default) + .toggle_state(matches!(self.mode, NewSessionMode::Configure)) + .style(ui::ButtonStyle::Subtle) + .on_click(cx.listener(|this, _, window, cx| { + this.mode = NewSessionMode::Configure; + this.mode_focus_handle(cx).focus(window); + cx.notify(); + })) + .last(), ), ) .justify_between() @@ -675,83 +724,83 @@ impl Render for NewSessionModal { .border_b_1(), ) .child(v_flex().child(self.render_mode(window, cx))) - .child( - h_flex() + .map(|el| { + let container = h_flex() .justify_between() .gap_2() .p_2() .border_color(cx.theme().colors().border_variant) .border_t_1() - .w_full() - .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 => h_flex() + .w_full(); + match self.mode { + NewSessionMode::Configure => el.child( + container .child( - Button::new("new-session-modal-back", "Save to .zed/debug.json...") + h_flex() + .child( + Button::new( + "new-session-modal-back", + "Save to .zed/debug.json...", + ) + .on_click(cx.listener(|this, _, window, cx| { + this.save_debug_scenario(window, cx); + })) + .disabled( + self.debugger.is_none() + || self + .configure_mode + .read(cx) + .program + .read(cx) + .is_empty(cx) + || self.save_scenario_state.is_some(), + ), + ) + .child(self.render_save_state(cx)), + ) + .child( + Button::new("debugger-spawn", "Start") .on_click(cx.listener(|this, _, window, cx| { - this.save_debug_scenario(window, cx); + this.start_new_session(window, cx) })) .disabled( self.debugger.is_none() || self - .custom_mode + .configure_mode .read(cx) .program .read(cx) - .is_empty(cx) - || self.save_scenario_state.is_some(), + .is_empty(cx), ), - ) - .child(self.render_save_state(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) - } - }), + ), ), - ) + NewSessionMode::Attach => el.child( + container + .child(div().child(self.adapter_drop_down_menu(window, cx))) + .child( + Button::new("debugger-spawn", "Start") + .on_click(cx.listener(|this, _, window, cx| { + this.start_new_session(window, cx) + })) + .disabled( + self.debugger.is_none() + || self + .attach_mode + .read(cx) + .attach_picker + .read(cx) + .picker + .read(cx) + .delegate + .match_count() + == 0, + ), + ), + ), + NewSessionMode::Launch => el, + NewSessionMode::Task => el, + } + }) } } @@ -774,13 +823,13 @@ impl RenderOnce for AttachMode { } #[derive(Clone)] -pub(super) struct CustomMode { +pub(super) struct ConfigureMode { program: Entity, cwd: Entity, stop_on_entry: ToggleState, } -impl CustomMode { +impl ConfigureMode { pub(super) fn new( past_launch_config: Option, window: &mut Window, @@ -940,6 +989,11 @@ impl AttachMode { } } +#[derive(Clone)] +pub(super) struct TaskMode { + pub(super) task_modal: Entity, +} + pub(super) struct DebugScenarioDelegate { task_store: Entity, candidates: Vec<(Option, DebugScenario)>, @@ -995,12 +1049,12 @@ impl DebugScenarioDelegate { pub fn task_contexts_loaded( &mut self, - task_contexts: TaskContexts, + task_contexts: Arc, languages: Arc, _window: &mut Window, cx: &mut Context>, ) { - self.task_contexts = Some(Arc::new(task_contexts)); + self.task_contexts = Some(task_contexts); let (recent, scenarios) = self .task_store @@ -1206,7 +1260,7 @@ pub(crate) fn resolve_path(path: &mut String) { #[cfg(test)] impl NewSessionModal { - pub(crate) fn set_custom( + pub(crate) fn set_configure( &mut self, program: impl AsRef, cwd: impl AsRef, @@ -1214,21 +1268,21 @@ impl NewSessionModal { window: &mut Window, cx: &mut Context, ) { - self.mode = NewSessionMode::Custom; + self.mode = NewSessionMode::Configure; self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into())); - self.custom_mode.update(cx, |custom, cx| { - custom.program.update(cx, |editor, cx| { + self.configure_mode.update(cx, |configure, cx| { + configure.program.update(cx, |editor, cx| { editor.clear(window, cx); editor.set_text(program.as_ref(), window, cx); }); - custom.cwd.update(cx, |editor, cx| { + configure.cwd.update(cx, |editor, cx| { editor.clear(window, cx); editor.set_text(cwd.as_ref(), window, cx); }); - custom.stop_on_entry = match stop_on_entry { + configure.stop_on_entry = match stop_on_entry { true => ToggleState::Selected, _ => ToggleState::Unselected, } @@ -1239,28 +1293,3 @@ impl NewSessionModal { self.save_debug_scenario(window, cx); } } - -#[cfg(test)] -mod tests { - use paths::home_dir; - - #[test] - fn test_normalize_paths() { - let sep = std::path::MAIN_SEPARATOR; - let home = home_dir().to_string_lossy().to_string(); - let resolve_path = |path: &str| -> String { - let mut path = path.to_string(); - super::resolve_path(&mut path); - path - }; - - assert_eq!(resolve_path("bin"), format!("bin")); - assert_eq!(resolve_path(&format!("{sep}foo")), format!("{sep}foo")); - assert_eq!(resolve_path(""), format!("")); - assert_eq!( - resolve_path(&format!("~{sep}blah")), - format!("{home}{sep}blah") - ); - assert_eq!(resolve_path("~"), home); - } -} diff --git a/crates/debugger_ui/src/tests/new_session_modal.rs b/crates/debugger_ui/src/tests/new_session_modal.rs index ffdce0dbc4..11dc9a7370 100644 --- a/crates/debugger_ui/src/tests/new_session_modal.rs +++ b/crates/debugger_ui/src/tests/new_session_modal.rs @@ -7,6 +7,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use task::{DebugRequest, DebugScenario, LaunchRequest, TaskContext, VariableName, ZedDebugConfig}; use util::path; +use crate::new_session_modal::NewSessionMode; use crate::tests::{init_test, init_test_workspace}; #[gpui::test] @@ -170,7 +171,13 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut workspace .update(cx, |workspace, window, cx| { - crate::new_session_modal::NewSessionModal::show(workspace, window, cx); + crate::new_session_modal::NewSessionModal::show( + workspace, + window, + NewSessionMode::Launch, + None, + cx, + ); }) .unwrap(); @@ -184,7 +191,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut .expect("Modal should be active"); modal.update_in(cx, |modal, window, cx| { - modal.set_custom("/project/main", "/project", false, window, cx); + modal.set_configure("/project/main", "/project", false, window, cx); modal.save_scenario(window, cx); }); @@ -213,7 +220,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut pretty_assertions::assert_eq!(expected_content, actual_lines); modal.update_in(cx, |modal, window, cx| { - modal.set_custom("/project/other", "/project", true, window, cx); + modal.set_configure("/project/other", "/project", true, window, cx); modal.save_scenario(window, cx); }); diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index ff42924b7b..c124e01c50 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -27,7 +27,7 @@ /// /// The keybindings themselves are managed independently by calling cx.bind_keys(). /// (Though mostly when developing Zed itself, you just need to add a new line to -/// assets/keymaps/default.json). +/// assets/keymaps/default-{platform}.json). /// /// ```rust /// cx.bind_keys([ diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index f74825f649..ecdab689dc 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -23,7 +23,7 @@ use workspace::{ModalView, Workspace}; pub use zed_actions::{Rerun, Spawn}; /// A modal used to spawn new tasks. -pub(crate) struct TasksModalDelegate { +pub struct TasksModalDelegate { task_store: Entity, candidates: Option>, task_overrides: Option, @@ -33,21 +33,21 @@ pub(crate) struct TasksModalDelegate { selected_index: usize, workspace: WeakEntity, prompt: String, - task_contexts: TaskContexts, + task_contexts: Arc, placeholder_text: Arc, } /// Task template amendments to do before resolving the context. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub(crate) struct TaskOverrides { +pub struct TaskOverrides { /// See [`RevealTarget`]. - pub(crate) reveal_target: Option, + pub reveal_target: Option, } impl TasksModalDelegate { fn new( task_store: Entity, - task_contexts: TaskContexts, + task_contexts: Arc, task_overrides: Option, workspace: WeakEntity, ) -> Self { @@ -123,15 +123,16 @@ impl TasksModalDelegate { } pub struct TasksModal { - picker: Entity>, + pub picker: Entity>, _subscription: [Subscription; 2], } impl TasksModal { - pub(crate) fn new( + pub fn new( task_store: Entity, - task_contexts: TaskContexts, + task_contexts: Arc, task_overrides: Option, + is_modal: bool, workspace: WeakEntity, window: &mut Window, cx: &mut Context, @@ -142,6 +143,7 @@ impl TasksModal { window, cx, ) + .modal(is_modal) }); let _subscription = [ cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| { @@ -158,6 +160,20 @@ impl TasksModal { _subscription, } } + + pub fn task_contexts_loaded( + &mut self, + task_contexts: Arc, + window: &mut Window, + cx: &mut Context, + ) { + self.picker.update(cx, |picker, cx| { + picker.delegate.task_contexts = task_contexts; + picker.delegate.candidates = None; + picker.refresh(window, cx); + cx.notify(); + }) + } } impl Render for TasksModal { @@ -568,6 +584,7 @@ impl PickerDelegate for TasksModalDelegate { Vec::new() } } + fn render_footer( &self, window: &mut Window, diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 1eb067b2e7..6955f770a9 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -1,16 +1,15 @@ -use std::path::Path; +use std::{path::Path, sync::Arc}; use collections::HashMap; use editor::Editor; use gpui::{App, AppContext as _, Context, Entity, Task, Window}; -use modal::TaskOverrides; use project::{Location, TaskContexts, TaskSourceKind, Worktree}; use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName}; use workspace::Workspace; mod modal; -pub use modal::{Rerun, ShowAttachModal, Spawn, TasksModal}; +pub use modal::{Rerun, ShowAttachModal, Spawn, TaskOverrides, TasksModal}; pub fn init(cx: &mut App) { cx.observe_new( @@ -95,6 +94,11 @@ fn spawn_task_or_modal( window: &mut Window, cx: &mut Context, ) { + if let Some(provider) = workspace.debugger_provider() { + provider.spawn_task_or_modal(workspace, action, window, cx); + return; + } + match action { Spawn::ByName { task_name, @@ -143,7 +147,7 @@ pub fn toggle_modal( if can_open_modal { let task_contexts = task_contexts(workspace, window, cx); cx.spawn_in(window, async move |workspace, cx| { - let task_contexts = task_contexts.await; + let task_contexts = Arc::new(task_contexts.await); workspace .update_in(cx, |workspace, window, cx| { workspace.toggle_modal(window, cx, |window, cx| { @@ -153,6 +157,7 @@ pub fn toggle_modal( reveal_target.map(|target| TaskOverrides { reveal_target: Some(target), }), + true, workspace_handle, window, cx, @@ -166,7 +171,7 @@ pub fn toggle_modal( } } -fn spawn_tasks_filtered( +pub fn spawn_tasks_filtered( mut predicate: F, overrides: Option, window: &mut Window, diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs index 4795af827c..4134e7ed74 100644 --- a/crates/workspace/src/tasks.rs +++ b/crates/workspace/src/tasks.rs @@ -56,6 +56,10 @@ impl Workspace { ) { let spawn_in_terminal = resolved_task.resolved.clone(); if !omit_history { + if let Some(debugger_provider) = self.debugger_provider.as_ref() { + debugger_provider.task_scheduled(cx); + } + self.project().update(cx, |project, cx| { if let Some(task_inventory) = project.task_store().read(cx).task_inventory().cloned() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e8cdf7aa4f..e30222dab4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -100,13 +100,13 @@ use task::{DebugScenario, SpawnInTerminal, TaskContext}; use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; -use ui::prelude::*; +use ui::{Window, prelude::*}; use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true}; use uuid::Uuid; pub use workspace_settings::{ AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings, }; -use zed_actions::feedback::FileBugReport; +use zed_actions::{Spawn, feedback::FileBugReport}; use crate::notifications::NotificationId; use crate::persistence::{ @@ -149,6 +149,18 @@ pub trait DebuggerProvider { window: &mut Window, cx: &mut App, ); + + fn spawn_task_or_modal( + &self, + workspace: &mut Workspace, + action: &Spawn, + window: &mut Window, + cx: &mut Context, + ); + + fn task_scheduled(&self, cx: &mut App); + fn debug_scenario_scheduled(&self, cx: &mut App); + fn debug_scenario_scheduled_last(&self, cx: &App) -> bool; } actions!( @@ -947,7 +959,7 @@ pub struct Workspace { on_prompt_for_new_path: Option, on_prompt_for_open_path: Option, terminal_provider: Option>, - debugger_provider: Option>, + debugger_provider: Option>, serializable_items_tx: UnboundedSender>, serialized_ssh_project: Option, _items_serializer: Task>, @@ -1828,7 +1840,11 @@ impl Workspace { } pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) { - self.debugger_provider = Some(Box::new(provider)); + self.debugger_provider = Some(Arc::new(provider)); + } + + pub fn debugger_provider(&self) -> Option> { + self.debugger_provider.clone() } pub fn serialized_ssh_project(&self) -> Option { diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 7bfab92e9a..419b3320c5 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -133,6 +133,46 @@ impl Render for QuickActionBar { ) }); + let last_run_debug = self + .workspace + .read_with(cx, |workspace, cx| { + workspace + .debugger_provider() + .map(|provider| provider.debug_scenario_scheduled_last(cx)) + .unwrap_or_default() + }) + .ok() + .unwrap_or_default(); + + let run_button = if last_run_debug { + QuickActionBarButton::new( + "debug", + IconName::Debug, // TODO: use debug + play icon + false, + Box::new(debugger_ui::Start), + focus_handle.clone(), + "Debug", + move |_, window, cx| { + window.dispatch_action(Box::new(debugger_ui::Start), cx); + }, + ) + } else { + let action = Box::new(tasks_ui::Spawn::ViaModal { + reveal_target: None, + }); + QuickActionBarButton::new( + "run", + IconName::Play, + false, + action.boxed_clone(), + focus_handle.clone(), + "Spawn Task", + move |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx); + }, + ) + }; + let assistant_button = QuickActionBarButton::new( "toggle inline assistant", IconName::ZedAssistant, @@ -561,6 +601,7 @@ impl Render for QuickActionBar { AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button, |bar| bar.child(assistant_button), ) + .child(run_button) .children(code_actions_dropdown) .children(editor_selections_dropdown) .child(editor_settings_dropdown)