diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 12a31a7360bb6e7fb1ff6fdcaf18f73d679693a3..f1a1b4dc738f82f729832c60648392af8b9921ed 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -134,6 +134,10 @@ impl DebugPanel { .map(|session| session.read(cx).running_state().clone()) } + pub fn project(&self) -> &Entity { + &self.project + } + pub fn load( workspace: WeakEntity, cx: &mut AsyncWindowContext, diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index eeed36ac1df18d5a0d1b33d6c3567c7df6a4b9c0..f1fa4738e30e5ed24e7815b61571b03e5a16252e 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, bail}; use collections::{FxHashMap, HashMap, HashSet}; -use language::LanguageRegistry; +use language::{LanguageName, LanguageRegistry}; use std::{ borrow::Cow, path::{Path, PathBuf}, @@ -22,7 +22,7 @@ use itertools::Itertools as _; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore}; use settings::Settings; -use task::{DebugScenario, RevealTarget, ZedDebugConfig}; +use task::{DebugScenario, RevealTarget, VariableName, ZedDebugConfig}; use theme::ThemeSettings; use ui::{ ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, @@ -978,6 +978,7 @@ pub(super) struct DebugDelegate { task_store: Entity, candidates: Vec<( Option, + Option, DebugScenario, Option, )>, @@ -1005,28 +1006,89 @@ impl DebugDelegate { } } - fn get_scenario_kind( + fn get_task_subtitle( + &self, + task_kind: &Option, + context: &Option, + cx: &mut App, + ) -> Option { + match task_kind { + Some(TaskSourceKind::Worktree { + id: worktree_id, + directory_in_worktree, + .. + }) => self + .debug_panel + .update(cx, |debug_panel, cx| { + let project = debug_panel.project().read(cx); + let worktrees: Vec<_> = project.visible_worktrees(cx).collect(); + + let mut path = if worktrees.len() > 1 + && let Some(worktree) = project.worktree_for_id(*worktree_id, cx) + { + let worktree_path = worktree.read(cx).abs_path(); + let full_path = worktree_path.join(directory_in_worktree); + full_path + } else { + directory_in_worktree.clone() + }; + + match path + .components() + .next_back() + .and_then(|component| component.as_os_str().to_str()) + { + Some(".zed") => { + path.push("debug.json"); + } + Some(".vscode") => { + path.push("launch.json"); + } + _ => {} + } + Some(path.display().to_string()) + }) + .unwrap_or_else(|_| Some(directory_in_worktree.display().to_string())), + Some(TaskSourceKind::AbsPath { abs_path, .. }) => { + Some(abs_path.to_string_lossy().into_owned()) + } + Some(TaskSourceKind::Lsp { language_name, .. }) => { + Some(format!("LSP: {language_name}")) + } + Some(TaskSourceKind::Language { .. }) => None, + _ => context.clone().and_then(|ctx| { + ctx.task_context + .task_variables + .get(&VariableName::RelativeFile) + .map(|f| format!("in {f}")) + .or_else(|| { + ctx.task_context + .task_variables + .get(&VariableName::Dirname) + .map(|d| format!("in {d}/")) + }) + }), + } + } + + fn get_scenario_language( languages: &Arc, dap_registry: &DapRegistry, scenario: DebugScenario, - ) -> (Option, DebugScenario) { + ) -> (Option, DebugScenario) { let language_names = languages.language_names(); - let language = dap_registry - .adapter_language(&scenario.adapter) - .map(|language| TaskSourceKind::Language { name: language.0 }); + let language_name = dap_registry.adapter_language(&scenario.adapter); - let language = language.or_else(|| { + let language_name = language_name.or_else(|| { scenario.label.split_whitespace().find_map(|word| { language_names .iter() .find(|name| name.as_ref().eq_ignore_ascii_case(word)) - .map(|name| TaskSourceKind::Language { - name: name.to_owned().into(), - }) + .cloned() }) }); - (language, scenario) + (language_name, scenario) } pub fn tasks_loaded( @@ -1080,9 +1142,9 @@ impl DebugDelegate { this.delegate.candidates = recent .into_iter() .map(|(scenario, context)| { - let (kind, scenario) = - Self::get_scenario_kind(&languages, dap_registry, scenario); - (kind, scenario, Some(context)) + let (language_name, scenario) = + Self::get_scenario_language(&languages, dap_registry, scenario); + (None, language_name, scenario, Some(context)) }) .chain( scenarios @@ -1097,9 +1159,9 @@ impl DebugDelegate { }) .filter(|(_, scenario)| valid_adapters.contains(&scenario.adapter)) .map(|(kind, scenario)| { - let (language, scenario) = - Self::get_scenario_kind(&languages, dap_registry, scenario); - (language.or(Some(kind)), scenario, None) + let (language_name, scenario) = + Self::get_scenario_language(&languages, dap_registry, scenario); + (Some(kind), language_name, scenario, None) }), ) .collect(); @@ -1145,7 +1207,7 @@ impl PickerDelegate for DebugDelegate { let candidates: Vec<_> = candidates .into_iter() .enumerate() - .map(|(index, (_, candidate, _))| { + .map(|(index, (_, _, candidate, _))| { StringMatchCandidate::new(index, candidate.label.as_ref()) }) .collect(); @@ -1314,7 +1376,7 @@ impl PickerDelegate for DebugDelegate { .get(self.selected_index()) .and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned()); - let Some((kind, debug_scenario, context)) = debug_scenario else { + let Some((kind, _, debug_scenario, context)) = debug_scenario else { return; }; @@ -1447,6 +1509,7 @@ impl PickerDelegate for DebugDelegate { cx: &mut Context>, ) -> Option { let hit = &self.matches.get(ix)?; + let (task_kind, language_name, _scenario, context) = &self.candidates[hit.candidate_id]; let highlighted_location = HighlightedMatch { text: hit.string.clone(), @@ -1454,33 +1517,40 @@ impl PickerDelegate for DebugDelegate { char_count: hit.string.chars().count(), color: Color::Default, }; - let task_kind = &self.candidates[hit.candidate_id].0; - - let icon = match task_kind { - 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::Lsp { - language_name: name, - .. - }) - | 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(IconSize::Small)); - let indicator = if matches!(task_kind, Some(TaskSourceKind::Lsp { .. })) { - Some(Indicator::icon( - Icon::new(IconName::BoltFilled) - .color(Color::Muted) - .size(IconSize::Small), - )) - } else { - None + + let subtitle = self.get_task_subtitle(task_kind, context, cx); + + let language_icon = language_name.as_ref().and_then(|lang| { + file_icons::FileIcons::get(cx) + .get_icon_for_type(&lang.0.to_lowercase(), cx) + .map(Icon::from_path) + }); + + let (icon, indicator) = match task_kind { + Some(TaskSourceKind::UserInput) => (Some(Icon::new(IconName::Terminal)), None), + Some(TaskSourceKind::AbsPath { .. }) => (Some(Icon::new(IconName::Settings)), None), + Some(TaskSourceKind::Worktree { .. }) => (Some(Icon::new(IconName::FileTree)), None), + Some(TaskSourceKind::Lsp { language_name, .. }) => ( + file_icons::FileIcons::get(cx) + .get_icon_for_type(&language_name.to_lowercase(), cx) + .map(Icon::from_path), + Some(Indicator::icon( + Icon::new(IconName::BoltFilled) + .color(Color::Muted) + .size(IconSize::Small), + )), + ), + Some(TaskSourceKind::Language { name }) => ( + file_icons::FileIcons::get(cx) + .get_icon_for_type(&name.to_lowercase(), cx) + .map(Icon::from_path), + None, + ), + None => (Some(Icon::new(IconName::HistoryRerun)), None), }; - let icon = icon.map(|icon| { - IconWithIndicator::new(icon, indicator) + + let icon = language_icon.or(icon).map(|icon| { + IconWithIndicator::new(icon.color(Color::Muted).size(IconSize::Small), indicator) .indicator_border_color(Some(cx.theme().colors().border_transparent)) }); @@ -1490,7 +1560,18 @@ impl PickerDelegate for DebugDelegate { .start_slot::(icon) .spacing(ListItemSpacing::Sparse) .toggle_state(selected) - .child(highlighted_location.render(window, cx)), + .child( + v_flex() + .items_start() + .child(highlighted_location.render(window, cx)) + .when_some(subtitle, |this, subtitle_text| { + this.child( + Label::new(subtitle_text) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }), + ), ) } } @@ -1539,4 +1620,17 @@ impl NewProcessModal { } }) } + + pub(crate) fn debug_picker_candidate_subtitles(&self, cx: &mut App) -> Vec { + self.debug_picker.update(cx, |picker, cx| { + picker + .delegate + .candidates + .iter() + .filter_map(|(task_kind, _, _, context)| { + picker.delegate.get_task_subtitle(task_kind, context, cx) + }) + .collect() + }) + } } diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index bfc445cf67329b7190f8e5b8d353415fb53fcd74..80e27ee6bdeb1d1a2627ad7aa46bf68c38464510 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -10,6 +10,7 @@ use text::Point; use util::path; use crate::NewProcessMode; +use crate::new_process_modal::NewProcessModal; use crate::tests::{init_test, init_test_workspace}; #[gpui::test] @@ -178,13 +179,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut workspace .update(cx, |workspace, window, cx| { - crate::new_process_modal::NewProcessModal::show( - workspace, - window, - NewProcessMode::Debug, - None, - cx, - ); + NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); }) .unwrap(); @@ -192,7 +187,7 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut let modal = workspace .update(cx, |workspace, _, cx| { - workspace.active_modal::(cx) + workspace.active_modal::(cx) }) .unwrap() .expect("Modal should be active"); @@ -281,6 +276,73 @@ async fn test_save_debug_scenario_to_file(executor: BackgroundExecutor, cx: &mut pretty_assertions::assert_eq!(expected_content, debug_json_content); } +#[gpui::test] +async fn test_debug_modal_subtitles_with_multiple_worktrees( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + fs.insert_tree( + path!("/workspace1"), + json!({ + ".zed": { + "debug.json": r#"[ + { + "adapter": "fake-adapter", + "label": "Debug App 1", + "request": "launch", + "program": "./app1", + "cwd": "." + }, + { + "adapter": "fake-adapter", + "label": "Debug Tests 1", + "request": "launch", + "program": "./test1", + "cwd": "." + } + ]"# + }, + "main.rs": "fn main() {}" + }), + ) + .await; + + let project = Project::test(fs.clone(), [path!("/workspace1").as_ref()], cx).await; + + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + workspace + .update(cx, |workspace, window, cx| { + NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx); + }) + .unwrap(); + + cx.run_until_parked(); + + let modal = workspace + .update(cx, |workspace, _, cx| { + workspace.active_modal::(cx) + }) + .unwrap() + .expect("Modal should be active"); + + cx.executor().run_until_parked(); + + let subtitles = modal.update_in(cx, |modal, _, cx| { + modal.debug_picker_candidate_subtitles(cx) + }); + + assert_eq!( + subtitles.as_slice(), + [path!(".zed/debug.json"), path!(".zed/debug.json")] + ); +} + #[gpui::test] async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 818a5c58cff7a57705a6498581e62f010ff6dcf9..4d85c8f2622c7e287c41f28089845b99d7b2ec4d 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -388,6 +388,7 @@ impl Inventory { .into_iter() .flat_map(|worktree| self.worktree_templates_from_settings(worktree)) .collect::>(); + let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name().into(), }); diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index e14b42af2b673ae161b5434e0c4f9b3f096e316a..3522e9522a6d32d729e7f0dca6731b2052f63f94 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -493,7 +493,7 @@ impl PickerDelegate for TasksModalDelegate { language_name: name, .. } - | TaskSourceKind::Language { name } => file_icons::FileIcons::get(cx) + | TaskSourceKind::Language { name, .. } => file_icons::FileIcons::get(cx) .get_icon_for_type(&name.to_lowercase(), cx) .map(Icon::from_path), }