diff --git a/Cargo.lock b/Cargo.lock index d310d61151..f8b1038280 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4167,6 +4167,7 @@ dependencies = [ "editor", "env_logger 0.11.8", "feature_flags", + "file_icons", "futures 0.3.31", "fuzzy", "gpui", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index b88d31b0a1..ec2843531b 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -36,6 +36,7 @@ dap_adapters = { workspace = true, optional = true } db.workspace = true editor.workspace = true feature_flags.workspace = true +file_icons.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1741820385..082b68fb14 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -218,6 +218,18 @@ impl DebugPanel { cx, ) }); + if let Some(inventory) = self + .project + .read(cx) + .task_store() + .read(cx) + .task_inventory() + .cloned() + { + inventory.update(cx, |inventory, _| { + inventory.scenario_scheduled(scenario.clone()); + }) + } let task = cx.spawn_in(window, { let session = session.clone(); async move |this, cx| { diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs index f1f4ec7571..0238db4c76 100644 --- a/crates/debugger_ui/src/new_session_modal.rs +++ b/crates/debugger_ui/src/new_session_modal.rs @@ -3,7 +3,6 @@ use std::{ borrow::Cow, ops::Not, path::{Path, PathBuf}, - sync::Arc, time::Duration, usize, }; @@ -50,7 +49,6 @@ pub(super) struct NewSessionModal { attach_mode: Entity, custom_mode: Entity, debugger: Option, - task_contexts: Arc, save_scenario_state: Option, _subscriptions: [Subscription; 2], } @@ -85,14 +83,6 @@ impl NewSessionModal { let task_store = workspace.project().read(cx).task_store().clone(); cx.spawn_in(window, async move |workspace, cx| { - let task_contexts = Arc::from( - workspace - .update_in(cx, |workspace, window, cx| { - tasks_ui::task_contexts(workspace, window, cx) - })? - .await, - ); - workspace.update_in(cx, |workspace, window, cx| { let workspace_handle = workspace.weak_handle(); workspace.toggle_modal(window, cx, |window, cx| { @@ -100,12 +90,7 @@ impl NewSessionModal { let launch_picker = cx.new(|cx| { Picker::uniform_list( - DebugScenarioDelegate::new( - debug_panel.downgrade(), - workspace_handle.clone(), - task_store, - task_contexts.clone(), - ), + DebugScenarioDelegate::new(debug_panel.downgrade(), task_store), window, cx, ) @@ -124,11 +109,38 @@ impl NewSessionModal { ), ]; - let active_cwd = task_contexts - .active_context() - .and_then(|context| context.cwd.clone()); + let custom_mode = CustomMode::new(None, window, cx); - let custom_mode = CustomMode::new(None, active_cwd, 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.launch_picker.update(cx, |picker, cx| { + picker + .delegate + .task_contexts_loaded(task_contexts, window, cx); + picker.refresh(window, cx); + cx.notify(); + }); + }) + } + }) + .detach(); Self { launch_picker, @@ -138,7 +150,6 @@ impl NewSessionModal { mode: NewSessionMode::Launch, debug_panel: debug_panel.downgrade(), workspace: workspace_handle, - task_contexts, save_scenario_state: None, _subscriptions, } @@ -205,8 +216,6 @@ impl NewSessionModal { fn start_new_session(&self, window: &mut Window, cx: &mut Context) { let Some(debugger) = self.debugger.as_ref() else { - // todo(debugger): show in UI. - log::error!("No debugger selected"); return; }; @@ -223,10 +232,12 @@ impl NewSessionModal { }; let debug_panel = self.debug_panel.clone(); - let task_contexts = self.task_contexts.clone(); + let Some(task_contexts) = self.task_contexts(cx) else { + return; + }; + let task_context = task_contexts.active_context().cloned().unwrap_or_default(); + let worktree_id = task_contexts.worktree(); cx.spawn_in(window, async move |this, cx| { - let task_context = task_contexts.active_context().cloned().unwrap_or_default(); - let worktree_id = task_contexts.worktree(); debug_panel.update_in(cx, |debug_panel, window, cx| { debug_panel.start_session(config, task_context, None, worktree_id, window, cx) })?; @@ -260,6 +271,11 @@ impl NewSessionModal { cx.notify(); }) } + + fn task_contexts<'a>(&self, cx: &'a mut Context) -> Option<&'a TaskContexts> { + self.launch_picker.read(cx).delegate.task_contexts.as_ref() + } + fn adapter_drop_down_menu( &mut self, window: &mut Window, @@ -267,15 +283,14 @@ impl NewSessionModal { ) -> ui::DropdownMenu { let workspace = self.workspace.clone(); let weak = cx.weak_entity(); - let active_buffer_language = self - .task_contexts - .active_item_context - .as_ref() - .and_then(|item| { - item.1 - .as_ref() - .and_then(|location| location.buffer.read(cx).language()) - }) + let active_buffer = self.task_contexts(cx).and_then(|tc| { + tc.active_item_context + .as_ref() + .and_then(|aic| aic.1.as_ref().map(|l| l.buffer.clone())) + }); + + let active_buffer_language = active_buffer + .and_then(|buffer| buffer.read(cx).language()) .cloned(); let mut available_adapters = workspace @@ -515,7 +530,10 @@ impl Render for NewSessionModal { .debugger .as_ref() .and_then(|debugger| this.debug_scenario(&debugger, cx)) - .zip(this.task_contexts.worktree()) + .zip( + this.task_contexts(cx) + .and_then(|tcx| tcx.worktree()), + ) .and_then(|(scenario, worktree_id)| { this.debug_panel .update(cx, |panel, cx| { @@ -715,13 +733,12 @@ pub(super) struct CustomMode { impl CustomMode { pub(super) fn new( past_launch_config: Option, - active_cwd: 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, active_cwd)); + .unwrap_or_else(|| (None, None)); let program = cx.new(|cx| Editor::single_line(window, cx)); program.update(cx, |this, cx| { @@ -745,6 +762,14 @@ impl CustomMode { }) } + fn load(&mut self, cwd: PathBuf, window: &mut Window, cx: &mut App) { + self.cwd.update(cx, |editor, cx| { + if editor.is_empty(cx) { + editor.set_text(cwd.to_string_lossy(), window, cx); + } + }); + } + pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest { let path = self.cwd.read(cx).text(cx); if cfg!(windows) { @@ -894,33 +919,64 @@ impl AttachMode { pub(super) struct DebugScenarioDelegate { task_store: Entity, - candidates: Option>, + candidates: Vec<(Option, DebugScenario)>, selected_index: usize, matches: Vec, prompt: String, debug_panel: WeakEntity, - workspace: WeakEntity, - task_contexts: Arc, + task_contexts: Option, + divider_index: Option, + last_used_candidate_index: Option, } impl DebugScenarioDelegate { - pub(super) fn new( - debug_panel: WeakEntity, - workspace: WeakEntity, - task_store: Entity, - task_contexts: Arc, - ) -> Self { + pub(super) fn new(debug_panel: WeakEntity, task_store: Entity) -> Self { Self { task_store, - candidates: None, + candidates: Vec::default(), selected_index: 0, matches: Vec::new(), prompt: String::new(), debug_panel, - workspace, - task_contexts, + task_contexts: None, + divider_index: None, + last_used_candidate_index: None, } } + + pub fn task_contexts_loaded( + &mut self, + task_contexts: TaskContexts, + _window: &mut Window, + cx: &mut Context>, + ) { + self.task_contexts = Some(task_contexts); + + let (recent, scenarios) = self + .task_store + .update(cx, |task_store, cx| { + task_store.task_inventory().map(|inventory| { + inventory.update(cx, |inventory, cx| { + inventory.list_debug_scenarios(self.task_contexts.as_ref().unwrap(), cx) + }) + }) + }) + .unwrap_or_default(); + + if !recent.is_empty() { + self.last_used_candidate_index = Some(recent.len() - 1); + } + + self.candidates = recent + .into_iter() + .map(|scenario| (None, scenario)) + .chain( + scenarios + .into_iter() + .map(|(kind, scenario)| (Some(kind), scenario)), + ) + .collect(); + } } impl PickerDelegate for DebugScenarioDelegate { @@ -954,53 +1010,15 @@ impl PickerDelegate for DebugScenarioDelegate { cx: &mut Context>, ) -> gpui::Task<()> { let candidates = self.candidates.clone(); - let workspace = self.workspace.clone(); - let task_store = self.task_store.clone(); 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(), - 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() - } - }; + let candidates: Vec<_> = candidates + .into_iter() + .enumerate() + .map(|(index, (_, candidate))| { + StringMatchCandidate::new(index, candidate.label.as_ref()) + }) + .collect(); let matches = fuzzy::match_strings( &candidates, @@ -1019,6 +1037,13 @@ impl PickerDelegate for DebugScenarioDelegate { delegate.matches = matches; delegate.prompt = query; + delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| { + let index = delegate + .matches + .partition_point(|matching_task| matching_task.candidate_id <= index); + Some(index).and_then(|index| (index != 0).then(|| index - 1)) + }); + if delegate.matches.is_empty() { delegate.selected_index = 0; } else { @@ -1030,34 +1055,34 @@ impl PickerDelegate for DebugScenarioDelegate { }) } + fn separators_after_indices(&self) -> Vec { + if let Some(i) = self.divider_index { + vec![i] + } else { + Vec::new() + } + } + 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()) - }); + .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned()); - let Some((task_source_kind, debug_scenario)) = debug_scenario else { + let Some((_, debug_scenario)) = debug_scenario else { return; }; - let (task_context, worktree_id) = if let TaskSourceKind::Worktree { - id: worktree_id, - directory_in_worktree: _, - id_base: _, - } = task_source_kind - { - self.task_contexts - .task_context_for_worktree_id(worktree_id) - .cloned() - .map(|context| (context, Some(worktree_id))) - } else { - None - } - .unwrap_or_default(); + let (task_context, worktree_id) = self + .task_contexts + .as_ref() + .and_then(|task_contexts| { + Some(( + task_contexts.active_context().cloned()?, + task_contexts.worktree(), + )) + }) + .unwrap_or_default(); self.debug_panel .update(cx, |panel, cx| { @@ -1087,10 +1112,19 @@ impl PickerDelegate for DebugScenarioDelegate { char_count: hit.string.chars().count(), color: Color::Default, }; + let task_kind = &self.candidates[hit.candidate_id].0; - let icon = Icon::new(IconName::FileTree) - .color(Color::Muted) - .size(ui::IconSize::Small); + let icon = match task_kind { + Some(TaskSourceKind::Lsp(..)) => Some(Icon::new(IconName::Bolt)), + Some(TaskSourceKind::UserInput) => Some(Icon::new(IconName::Terminal)), + Some(TaskSourceKind::AbsPath { .. }) => Some(Icon::new(IconName::Settings)), + Some(TaskSourceKind::Worktree { .. }) => Some(Icon::new(IconName::FileTree)), + Some(TaskSourceKind::Language { name }) => file_icons::FileIcons::get(cx) + .get_icon_for_type(&name.to_lowercase(), cx) + .map(Icon::from_path), + None => Some(Icon::new(IconName::HistoryRerun)), + } + .map(|icon| icon.color(Color::Muted).size(ui::IconSize::Small)); Some( ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}"))) diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 379412557f..999fca6345 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1103,6 +1103,7 @@ impl CodeActionsMenu { this.child( h_flex() .overflow_hidden() + .child("debug: ") .child(scenario.label.clone()) .when(selected, |this| { this.text_color(colors.text_accent) @@ -1138,7 +1139,9 @@ impl CodeActionsMenu { CodeActionsItem::CodeAction { action, .. } => { action.lsp_action.title().chars().count() } - CodeActionsItem::DebugScenario(scenario) => scenario.label.chars().count(), + CodeActionsItem::DebugScenario(scenario) => { + format!("debug: {}", scenario.label).chars().count() + } }) .map(|(ix, _)| ix), ) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e0870ca012..6edc5970e0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5331,9 +5331,9 @@ impl Editor { .map(SharedString::from) })?; - dap_store.update(cx, |this, cx| { + dap_store.update(cx, |dap_store, cx| { for (_, task) in &resolved_tasks.templates { - if let Some(scenario) = this + if let Some(scenario) = dap_store .debug_scenario_for_build_task( task.original_task().clone(), debug_adapter.clone().into(), diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index 36deaec4d9..e0487e7c4a 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -52,7 +52,7 @@ impl DapLocator for CargoLocator { } let mut task_template = build_config.clone(); let cargo_action = task_template.args.first_mut()?; - if cargo_action == "check" { + if cargo_action == "check" || cargo_action == "clean" { return None; } @@ -75,10 +75,9 @@ impl DapLocator for CargoLocator { } _ => {} } - let label = format!("Debug `{resolved_label}`"); Some(DebugScenario { adapter: adapter.0, - label: SharedString::from(label), + label: resolved_label.to_string().into(), build: Some(BuildTaskDefinition::Template { task_template, locator_name: Some(self.name()), diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 88e1042be6..f53bc8e633 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -10,6 +10,7 @@ use std::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; +use dap::DapRegistry; use gpui::{App, AppContext as _, Entity, SharedString, Task}; use itertools::Itertools; use language::{ @@ -33,6 +34,7 @@ use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore}; #[derive(Debug, Default)] pub struct Inventory { last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>, + last_scheduled_scenarios: VecDeque, templates_from_settings: InventoryFor, scenarios_from_settings: InventoryFor, } @@ -63,30 +65,28 @@ struct InventoryFor { impl InventoryFor { fn worktree_scenarios( &self, - worktree: Option, + worktree: WorktreeId, ) -> impl '_ + Iterator { - worktree.into_iter().flat_map(|worktree| { - self.worktree - .get(&worktree) - .into_iter() - .flatten() - .flat_map(|(directory, templates)| { - templates.iter().map(move |template| (directory, template)) - }) - .map(move |(directory, template)| { - ( - TaskSourceKind::Worktree { - id: worktree, - directory_in_worktree: directory.to_path_buf(), - id_base: Cow::Owned(format!( - "local worktree {} from directory {directory:?}", - T::LABEL - )), - }, - template.clone(), - ) - }) - }) + self.worktree + .get(&worktree) + .into_iter() + .flatten() + .flat_map(|(directory, templates)| { + templates.iter().map(move |template| (directory, template)) + }) + .map(move |(directory, template)| { + ( + TaskSourceKind::Worktree { + id: worktree, + directory_in_worktree: directory.to_path_buf(), + id_base: Cow::Owned(format!( + "local worktree {} from directory {directory:?}", + T::LABEL + )), + }, + template.clone(), + ) + }) } fn global_scenarios(&self) -> impl '_ + Iterator { @@ -168,6 +168,13 @@ impl TaskContexts { .and_then(|(_, location, _)| location.as_ref()) } + pub fn file(&self, cx: &App) -> Option> { + self.active_item_context + .as_ref() + .and_then(|(_, location, _)| location.as_ref()) + .and_then(|location| location.buffer.read(cx).file().cloned()) + } + pub fn worktree(&self) -> Option { self.active_item_context .as_ref() @@ -214,16 +221,69 @@ impl Inventory { cx.new(|_| Self::default()) } + pub fn scenario_scheduled(&mut self, scenario: DebugScenario) { + self.last_scheduled_scenarios + .retain(|s| s.label != scenario.label); + self.last_scheduled_scenarios.push_back(scenario); + if self.last_scheduled_scenarios.len() > 5_000 { + self.last_scheduled_scenarios.pop_front(); + } + } + pub fn list_debug_scenarios( &self, - worktrees: impl Iterator, - ) -> Vec<(TaskSourceKind, DebugScenario)> { - let global_scenarios = self.global_debug_scenarios_from_settings(); + task_contexts: &TaskContexts, + cx: &mut App, + ) -> (Vec, Vec<(TaskSourceKind, DebugScenario)>) { + let mut scenarios = Vec::new(); - worktrees - .flat_map(|tree_id| self.worktree_scenarios_from_settings(Some(tree_id))) - .chain(global_scenarios) - .collect() + if let Some(worktree_id) = task_contexts + .active_worktree_context + .iter() + .chain(task_contexts.other_worktree_contexts.iter()) + .map(|context| context.0) + .next() + { + scenarios.extend(self.worktree_scenarios_from_settings(worktree_id)); + } + scenarios.extend(self.global_debug_scenarios_from_settings()); + + let (_, new) = self.used_and_current_resolved_tasks(task_contexts, cx); + if let Some(location) = task_contexts.location() { + let file = location.buffer.read(cx).file(); + let language = location.buffer.read(cx).language(); + let language_name = language.as_ref().map(|l| l.name()); + let adapter = language_settings(language_name, file, cx) + .debuggers + .first() + .map(SharedString::from) + .or_else(|| { + language.and_then(|l| l.config().debuggers.first().map(SharedString::from)) + }); + if let Some(adapter) = adapter { + for (kind, task) in new { + if let Some(scenario) = + DapRegistry::global(cx) + .locators() + .values() + .find_map(|locator| { + locator.create_scenario( + &task.original_task().clone(), + &task.display_label(), + adapter.clone().into(), + ) + }) + { + scenarios.push((kind, scenario)); + } + } + } + } + + ( + self.last_scheduled_scenarios.iter().cloned().collect(), + scenarios, + ) } pub fn task_template_by_label( @@ -262,7 +322,9 @@ impl Inventory { cx: &App, ) -> Vec<(TaskSourceKind, TaskTemplate)> { let global_tasks = self.global_templates_from_settings(); - let worktree_tasks = self.worktree_templates_from_settings(worktree); + let worktree_tasks = worktree + .into_iter() + .flat_map(|worktree| self.worktree_templates_from_settings(worktree)); let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name().into(), }); @@ -354,8 +416,9 @@ impl Inventory { .into_iter() .flat_map(|tasks| tasks.0.into_iter()) .flat_map(|task| Some((task_source_kind.clone()?, task))); - let worktree_tasks = self - .worktree_templates_from_settings(worktree) + let worktree_tasks = worktree + .into_iter() + .flat_map(|worktree| self.worktree_templates_from_settings(worktree)) .chain(language_tasks) .chain(global_tasks); @@ -471,14 +534,14 @@ impl Inventory { fn worktree_scenarios_from_settings( &self, - worktree: Option, + worktree: WorktreeId, ) -> impl '_ + Iterator { self.scenarios_from_settings.worktree_scenarios(worktree) } fn worktree_templates_from_settings( &self, - worktree: Option, + worktree: WorktreeId, ) -> impl '_ + Iterator { self.templates_from_settings.worktree_scenarios(worktree) }