diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 31a7a828b8..368c911cf0 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -7,6 +7,7 @@ use crate::{ }; use crate::{new_session_modal::NewSessionModal, session::DebugSession}; use anyhow::{Result, anyhow}; +use collections::{HashMap, HashSet}; use command_palette_hooks::CommandPaletteFilter; use dap::DebugRequest; use dap::{ @@ -26,6 +27,7 @@ use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; use settings::Settings; use std::any::TypeId; +use std::path::PathBuf; use task::{DebugScenario, TaskContext}; use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use workspace::SplitDirection; @@ -403,7 +405,6 @@ impl DebugPanel { pub fn resolve_scenario( &self, scenario: DebugScenario, - task_context: TaskContext, buffer: Option>, window: &Window, @@ -424,8 +425,60 @@ impl DebugPanel { stop_on_entry, } = scenario; let request = if let Some(mut request) = request { - // Resolve task variables within the request. - if let DebugRequest::Launch(_) = &mut request {} + if let DebugRequest::Launch(launch_config) = &mut request { + let mut variable_names = HashMap::default(); + let mut substituted_variables = HashSet::default(); + let task_variables = task_context + .task_variables + .iter() + .map(|(key, value)| { + let key_string = key.to_string(); + if !variable_names.contains_key(&key_string) { + variable_names.insert(key_string.clone(), key.clone()); + } + (key_string, value.as_str()) + }) + .collect::>(); + + let cwd = launch_config + .cwd + .as_ref() + .and_then(|cwd| cwd.to_str()) + .and_then(|cwd| { + task::substitute_all_template_variables_in_str( + cwd, + &task_variables, + &variable_names, + &mut substituted_variables, + ) + }); + + if let Some(cwd) = cwd { + launch_config.cwd = Some(PathBuf::from(cwd)) + } + + if let Some(program) = task::substitute_all_template_variables_in_str( + &launch_config.program, + &task_variables, + &variable_names, + &mut substituted_variables, + ) { + launch_config.program = program; + } + + for arg in launch_config.args.iter_mut() { + if let Some(substituted_arg) = + task::substitute_all_template_variables_in_str( + &arg, + &task_variables, + &variable_names, + &mut substituted_variables, + ) + { + *arg = substituted_arg; + } + } + } request } else if let Some(build) = build { @@ -944,6 +997,7 @@ impl DebugPanel { past_debug_definition, weak_panel, workspace, + None, window, cx, ) diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 60d8519c53..5fb1a2dca3 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -158,6 +158,7 @@ pub fn init(cx: &mut App) { debug_panel.read(cx).past_debug_definition.clone(), weak_panel, weak_workspace, + None, window, cx, ) @@ -166,14 +167,22 @@ pub fn init(cx: &mut App) { }, ) .register_action(|workspace: &mut Workspace, _: &Start, window, cx| { - tasks_ui::toggle_modal( - workspace, - None, - task::TaskModal::DebugModal, - window, - cx, - ) - .detach(); + 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(); + + workspace.toggle_modal(window, cx, |window, cx| { + NewSessionModal::new( + debug_panel.read(cx).past_debug_definition.clone(), + weak_panel, + weak_workspace, + Some(task_store), + window, + cx, + ) + }); + } }); }) }) diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs index d774575169..1769a919fd 100644 --- a/crates/debugger_ui/src/new_session_modal.rs +++ b/crates/debugger_ui/src/new_session_modal.rs @@ -6,19 +6,25 @@ use std::{ use dap::{DapRegistry, DebugRequest, adapters::DebugTaskDefinition}; use editor::{Editor, EditorElement, EditorStyle}; +use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle, - WeakEntity, + App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, + Subscription, TextStyle, WeakEntity, }; +use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; +use project::{TaskSourceKind, task_store::TaskStore}; +use session_modes::{AttachMode, DebugScenarioDelegate, LaunchMode}; use settings::Settings; -use task::{DebugScenario, LaunchRequest, TaskContext}; +use task::{DebugScenario, LaunchRequest}; use theme::ThemeSettings; use ui::{ ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, - ContextMenu, Disableable, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label, - LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton, - ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex, + ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, InteractiveElement, + IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing, ParentElement, RenderOnce, + SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Window, div, h_flex, + relative, rems, v_flex, }; +use util::ResultExt; use workspace::{ModalView, Workspace}; use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel}; @@ -57,6 +63,7 @@ impl NewSessionModal { past_debug_definition: Option, debug_panel: WeakEntity, workspace: WeakEntity, + task_store: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -73,6 +80,18 @@ impl NewSessionModal { _ => None, }; + 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, + ); + }); + }; + Self { workspace: workspace.clone(), debugger, @@ -86,10 +105,10 @@ impl NewSessionModal { } } - fn debug_config(&self, cx: &App, debugger: &str) -> DebugScenario { - let request = self.mode.debug_task(cx); + fn debug_config(&self, cx: &App, debugger: &str) -> Option { + let request = self.mode.debug_task(cx)?; let label = suggested_label(&request, debugger); - DebugScenario { + Some(DebugScenario { adapter: debugger.to_owned().into(), label, request: Some(request), @@ -100,21 +119,42 @@ impl NewSessionModal { _ => None, }, build: None, - } + }) } fn start_new_session(&self, window: &mut Window, cx: &mut Context) { let Some(debugger) = self.debugger.as_ref() else { - // todo: show in UI. + // todo(debugger): show in UI. log::error!("No debugger selected"); return; }; - let config = self.debug_config(cx, debugger); + + if let NewSessionMode::Scenario(picker) = &self.mode { + picker.update(cx, |picker, cx| { + picker.delegate.confirm(false, window, cx); + }); + return; + } + + let Some(config) = self.debug_config(cx, debugger) else { + log::error!("debug config not found in mode: {}", self.mode); + return; + }; + let debug_panel = self.debug_panel.clone(); + let workspace = self.workspace.clone(); cx.spawn_in(window, async move |this, cx| { + let task_contexts = workspace + .update_in(cx, |workspace, window, cx| { + tasks_ui::task_contexts(workspace, window, cx) + })? + .await; + + let task_context = task_contexts.active_context().cloned().unwrap_or_default(); + debug_panel.update_in(cx, |debug_panel, window, cx| { - debug_panel.start_session(config, TaskContext::default(), None, window, cx) + debug_panel.start_session(config, task_context, None, window, cx) })?; this.update(cx, |_, cx| { cx.emit(DismissEvent); @@ -256,9 +296,14 @@ impl NewSessionModal { .iter() .flat_map(|task_inventory| { task_inventory.read(cx).list_debug_scenarios( - worktree.as_ref().map(|worktree| worktree.read(cx).id()), + worktree + .as_ref() + .map(|worktree| worktree.read(cx).id()) + .iter() + .copied(), ) }) + .map(|(_source_kind, scenario)| scenario) .collect() }) .ok() @@ -277,102 +322,22 @@ impl NewSessionModal { } } -#[derive(Clone)] -struct LaunchMode { - program: Entity, - cwd: Entity, -} - -impl LaunchMode { - fn new( - past_launch_config: Option, - window: &mut Window, - cx: &mut App, - ) -> Entity { - let (past_program, past_cwd) = past_launch_config - .map(|config| (Some(config.program), config.cwd)) - .unwrap_or_else(|| (None, None)); - - let program = cx.new(|cx| Editor::single_line(window, cx)); - program.update(cx, |this, cx| { - this.set_placeholder_text("Program path", cx); - - if let Some(past_program) = past_program { - this.set_text(past_program, window, cx); - }; - }); - let cwd = cx.new(|cx| Editor::single_line(window, cx)); - cwd.update(cx, |this, cx| { - this.set_placeholder_text("Working Directory", cx); - if let Some(past_cwd) = past_cwd { - this.set_text(past_cwd.to_string_lossy(), window, cx); - }; - }); - cx.new(|_| Self { program, cwd }) - } - - fn debug_task(&self, cx: &App) -> task::LaunchRequest { - let path = self.cwd.read(cx).text(cx); - task::LaunchRequest { - program: self.program.read(cx).text(cx), - cwd: path.is_empty().not().then(|| PathBuf::from(path)), - args: Default::default(), - env: Default::default(), - } - } -} - -#[derive(Clone)] -struct AttachMode { - definition: DebugTaskDefinition, - attach_picker: Entity, -} - -impl AttachMode { - fn new( - debugger: Option, - workspace: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let definition = DebugTaskDefinition { - adapter: debugger.clone().unwrap_or_default(), - label: "Attach New Session Setup".into(), - request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }), - initialize_args: None, - tcp_connection: None, - stop_on_entry: Some(false), - }; - let attach_picker = cx.new(|cx| { - let modal = AttachModal::new(definition.clone(), workspace, false, window, cx); - window.focus(&modal.focus_handle(cx)); - - modal - }); - cx.new(|_| Self { - definition, - attach_picker, - }) - } - fn debug_task(&self) -> task::AttachRequest { - task::AttachRequest { process_id: None } - } -} - 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) -> DebugRequest { + fn debug_task(&self, cx: &App) -> Option { match self { - NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(), - NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(), + 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> { @@ -382,6 +347,78 @@ impl NewSessionMode { 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, + } + } +} + +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(), + }; + + write!(f, "{}", mode) + } } impl Focusable for NewSessionMode { @@ -389,6 +426,7 @@ impl Focusable for NewSessionMode { 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), } } } @@ -437,27 +475,14 @@ impl RenderOnce for NewSessionMode { 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(), } } } -impl NewSessionMode { - 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 render_editor(editor: &Entity, window: &mut Window, cx: &App) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); let theme = cx.theme(); @@ -519,6 +544,34 @@ impl Render for NewSessionModal { h_flex() .justify_start() .w_full() + .child( + ToggleButton::new("debugger-session-ui-picker-button", "Scenarios") + .size(ButtonSize::Default) + .style(ui::ButtonStyle::Subtle) + .toggle_state(matches!(self.mode, NewSessionMode::Scenario(_))) + .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, + ); + + cx.notify(); + })) + .first(), + ) .child( ToggleButton::new( "debugger-session-ui-launch-button", @@ -532,7 +585,7 @@ impl Render for NewSessionModal { this.mode.focus_handle(cx).focus(window); cx.notify(); })) - .first(), + .middle(), ) .child( ToggleButton::new( @@ -601,10 +654,21 @@ impl Render for NewSessionModal { }) .child( Button::new("debugger-spawn", "Start") - .on_click(cx.listener(|this, _, window, cx| { - this.start_new_session(window, cx); + .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(self.debugger.is_none()), + .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(), + }), ), ), ) @@ -619,3 +683,319 @@ impl Focusable for NewSessionModal { } impl ModalView for NewSessionModal {} + +// This module makes sure that the modes setup the correct subscriptions whenever they're created +mod session_modes { + use std::rc::Rc; + + use super::*; + + #[derive(Clone)] + #[non_exhaustive] + pub(super) struct LaunchMode { + pub(super) program: Entity, + pub(super) cwd: Entity, + } + + impl LaunchMode { + pub(super) fn new( + past_launch_config: Option, + window: &mut Window, + cx: &mut App, + ) -> Entity { + let (past_program, past_cwd) = past_launch_config + .map(|config| (Some(config.program), config.cwd)) + .unwrap_or_else(|| (None, None)); + + let program = cx.new(|cx| Editor::single_line(window, cx)); + program.update(cx, |this, cx| { + this.set_placeholder_text("Program path", cx); + + if let Some(past_program) = past_program { + this.set_text(past_program, window, cx); + }; + }); + let cwd = cx.new(|cx| Editor::single_line(window, cx)); + cwd.update(cx, |this, cx| { + this.set_placeholder_text("Working Directory", cx); + if let Some(past_cwd) = past_cwd { + this.set_text(past_cwd.to_string_lossy(), window, cx); + }; + }); + cx.new(|_| Self { program, cwd }) + } + + pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest { + let path = self.cwd.read(cx).text(cx); + task::LaunchRequest { + program: self.program.read(cx).text(cx), + cwd: path.is_empty().not().then(|| PathBuf::from(path)), + args: Default::default(), + env: Default::default(), + } + } + } + + #[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, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let definition = DebugTaskDefinition { + adapter: debugger.clone().unwrap_or_default(), + label: "Attach New Session Setup".into(), + request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }), + initialize_args: None, + tcp_connection: None, + stop_on_entry: Some(false), + }; + let attach_picker = cx.new(|cx| { + let modal = AttachModal::new(definition.clone(), workspace, false, window, cx); + window.focus(&modal.focus_handle(cx)); + + 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 { + task::AttachRequest { process_id: None } + } + } + + pub(super) struct DebugScenarioDelegate { + task_store: Entity, + candidates: Option>, + selected_index: usize, + matches: Vec, + prompt: String, + debug_panel: WeakEntity, + workspace: WeakEntity, + } + + impl DebugScenarioDelegate { + pub(super) fn new( + debug_panel: WeakEntity, + workspace: WeakEntity, + task_store: Entity, + ) -> Self { + Self { + task_store, + candidates: None, + selected_index: 0, + matches: Vec::new(), + prompt: String::new(), + debug_panel, + workspace, + } + } + } + + impl PickerDelegate for DebugScenarioDelegate { + type ListItem = ui::ListItem; + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _window: &mut Window, + _cx: &mut Context>, + ) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc { + "".into() + } + + fn update_matches( + &mut self, + query: String, + 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()); + + 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, + true, + 1000, + &Default::default(), + cx.background_executor().clone(), + ) + .await; + + picker + .update(cx, |picker, _| { + let delegate = &mut picker.delegate; + + delegate.matches = matches; + delegate.prompt = query; + + if delegate.matches.is_empty() { + delegate.selected_index = 0; + } else { + delegate.selected_index = + delegate.selected_index.min(delegate.matches.len() - 1); + } + }) + .log_err(); + }) + } + + fn confirm( + &mut self, + _: bool, + window: &mut Window, + cx: &mut Context>, + ) { + let debug_scenario = + self.matches + .get(self.selected_index()) + .and_then(|match_candidate| { + self.candidates + .as_ref() + .map(|candidates| candidates[match_candidate.candidate_id].clone()) + }); + + let Some((task_source_kind, debug_scenario)) = debug_scenario else { + return; + }; + + let task_context = 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() + }) + } else { + gpui::Task::ready(None) + }; + + cx.spawn_in(window, async move |this, cx| { + let task_context = 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, window, cx); + }) + .ok(); + + cx.emit(DismissEvent); + }) + .ok(); + }) + .detach(); + } + + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { + cx.emit(DismissEvent); + } + + fn render_match( + &self, + ix: usize, + selected: bool, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + let hit = &self.matches[ix]; + + let highlighted_location = HighlightedMatch { + text: hit.string.clone(), + highlight_positions: hit.positions.clone(), + char_count: hit.string.chars().count(), + color: Color::Default, + }; + + let icon = Icon::new(IconName::FileTree) + .color(Color::Muted) + .size(ui::IconSize::Small); + + Some( + ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}"))) + .inset(true) + .start_slot::(icon) + .spacing(ListItemSpacing::Sparse) + .toggle_state(selected) + .child(highlighted_location.render(window, cx)), + ) + } + } +} diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 54b50453ce..978d6f04b5 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -588,7 +588,9 @@ impl Picker { self.update_matches(query, window, cx); } editor::EditorEvent::Blurred => { - self.cancel(&menu::Cancel, window, cx); + if self.is_modal { + self.cancel(&menu::Cancel, window, cx); + } } _ => {} } diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index efeb447b6a..6bb7fd0d1c 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -179,6 +179,14 @@ impl TaskContexts { }) .copied() } + + pub fn task_context_for_worktree_id(&self, worktree_id: WorktreeId) -> Option<&TaskContext> { + self.active_worktree_context + .iter() + .chain(self.other_worktree_contexts.iter()) + .find(|(id, _)| *id == worktree_id) + .map(|(_, context)| context) + } } impl TaskSourceKind { @@ -206,13 +214,15 @@ impl Inventory { cx.new(|_| Self::default()) } - pub fn list_debug_scenarios(&self, worktree: Option) -> Vec { + pub fn list_debug_scenarios( + &self, + worktrees: impl Iterator, + ) -> Vec<(TaskSourceKind, DebugScenario)> { let global_scenarios = self.global_debug_scenarios_from_settings(); - let worktree_scenarios = self.worktree_scenarios_from_settings(worktree); - worktree_scenarios + worktrees + .flat_map(|tree_id| self.worktree_scenarios_from_settings(Some(tree_id))) .chain(global_scenarios) - .map(|(_, scenario)| scenario) .collect() } diff --git a/crates/task/src/lib.rs b/crates/task/src/lib.rs index 45d9afbcb2..c400f0e0f0 100644 --- a/crates/task/src/lib.rs +++ b/crates/task/src/lib.rs @@ -19,7 +19,8 @@ pub use debug_format::{ AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate, }; pub use task_template::{ - DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, + DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, + substitute_all_template_variables_in_str, }; pub use vscode_debug_format::VsCodeDebugTaskFile; pub use vscode_format::VsCodeTaskFile; @@ -266,6 +267,10 @@ impl TaskVariables { } }) } + + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } } impl FromIterator<(VariableName, String)> for TaskVariables { diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 2032a24fe5..b1958037fb 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -83,15 +83,6 @@ pub enum DebugArgsRequest { Attach(AttachRequest), } -#[derive(Clone, Debug, PartialEq, Eq)] -/// The type of task modal to spawn -pub enum TaskModal { - /// Show regular tasks - ScriptModal, - /// Show debug tasks - DebugModal, -} - /// What to do with the terminal pane and tab, after the command was started. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -302,7 +293,7 @@ fn to_hex_hash(object: impl Serialize) -> anyhow::Result { Ok(hex::encode(hasher.finalize())) } -fn substitute_all_template_variables_in_str>( +pub fn substitute_all_template_variables_in_str>( template_str: &str, task_variables: &HashMap, variable_names: &HashMap, diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index 5f34816854..aa6f8c77a0 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -10,7 +10,7 @@ use gpui::{ use itertools::Itertools; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use project::{TaskSourceKind, task_store::TaskStore}; -use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskModal, TaskTemplate}; +use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskTemplate}; use ui::{ ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon, IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize, @@ -34,8 +34,6 @@ pub(crate) struct TasksModalDelegate { prompt: String, task_contexts: TaskContexts, placeholder_text: Arc, - /// If this delegate is responsible for running a scripting task or a debugger - task_modal_type: TaskModal, } /// Task template amendments to do before resolving the context. @@ -50,7 +48,6 @@ impl TasksModalDelegate { task_store: Entity, task_contexts: TaskContexts, task_overrides: Option, - task_modal_type: TaskModal, workspace: WeakEntity, ) -> Self { let placeholder_text = if let Some(TaskOverrides { @@ -71,7 +68,6 @@ impl TasksModalDelegate { selected_index: 0, prompt: String::default(), task_contexts, - task_modal_type, task_overrides, placeholder_text, } @@ -136,19 +132,12 @@ impl TasksModal { task_contexts: TaskContexts, task_overrides: Option, workspace: WeakEntity, - task_modal_type: TaskModal, window: &mut Window, cx: &mut Context, ) -> Self { let picker = cx.new(|cx| { Picker::uniform_list( - TasksModalDelegate::new( - task_store, - task_contexts, - task_overrides, - task_modal_type, - workspace, - ), + TasksModalDelegate::new(task_store, task_contexts, task_overrides, workspace), window, cx, ) @@ -231,9 +220,8 @@ impl PickerDelegate for TasksModalDelegate { window: &mut Window, cx: &mut Context>, ) -> Task<()> { - let task_type = self.task_modal_type.clone(); let candidates = match &self.candidates { - Some(candidates) => Task::ready(string_match_candidates(candidates, task_type)), + Some(candidates) => Task::ready(string_match_candidates(candidates)), None => { if let Some(task_inventory) = self.task_store.read(cx).task_inventory().cloned() { let (used, current) = task_inventory @@ -276,8 +264,7 @@ impl PickerDelegate for TasksModalDelegate { }, )); new_candidates.extend(current); - let match_candidates = - string_match_candidates(&new_candidates, task_type); + let match_candidates = string_match_candidates(&new_candidates); let _ = picker.delegate.candidates.insert(new_candidates); match_candidates }) @@ -669,12 +656,10 @@ impl PickerDelegate for TasksModalDelegate { fn string_match_candidates<'a>( candidates: impl IntoIterator + 'a, - task_modal_type: TaskModal, ) -> Vec { candidates .into_iter() .enumerate() - .filter(|(_, (_, _))| task_modal_type == TaskModal::ScriptModal) .map(|(index, (_, candidate))| StringMatchCandidate::new(index, candidate.display_label())) .collect() } diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 527dc12d4d..7e976184c0 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -5,9 +5,7 @@ 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, TaskModal, TaskTemplate, TaskVariables, VariableName, -}; +use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName}; use workspace::Workspace; mod modal; @@ -83,7 +81,7 @@ pub fn init(cx: &mut App) { ); } } else { - toggle_modal(workspace, None, TaskModal::ScriptModal, window, cx).detach(); + toggle_modal(workspace, None, window, cx).detach(); }; }); }, @@ -125,21 +123,15 @@ fn spawn_task_or_modal( ) .detach_and_log_err(cx) } - Spawn::ViaModal { reveal_target } => toggle_modal( - workspace, - *reveal_target, - TaskModal::ScriptModal, - window, - cx, - ) - .detach(), + Spawn::ViaModal { reveal_target } => { + toggle_modal(workspace, *reveal_target, window, cx).detach() + } } } pub fn toggle_modal( workspace: &mut Workspace, reveal_target: Option, - task_type: TaskModal, window: &mut Window, cx: &mut Context, ) -> Task<()> { @@ -162,7 +154,6 @@ pub fn toggle_modal( reveal_target: Some(target), }), workspace_handle, - task_type, window, cx, )