mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
debugger_ui: Update new process modal to include more context about its source (#36650)
Closes #36280 Release Notes: - Added additional context to debug task selection Adding additional context when selecting a debug task to help with projects that have multiple config files with similar names for tasks. I think there is room for improvement, especially adding context for a LanguageTask type. I started but it looked like it would need to add a path value to that and wanted to make sure this was a good idea before working on that. Also any thoughts on the wording if you do like this format? --- <img width="1246" height="696" alt="image" src="https://github.com/user-attachments/assets/b42e3f45-cfdb-4cb1-8a7a-3c37f33f5ee2" /> --------- Co-authored-by: Anthony <anthony@zed.dev> Co-authored-by: Anthony <hello@anthonyeid.me>
This commit is contained in:
parent
6b8ed5bf28
commit
fc0eb882f7
5 changed files with 216 additions and 55 deletions
|
|
@ -134,6 +134,10 @@ impl DebugPanel {
|
|||
.map(|session| session.read(cx).running_state().clone())
|
||||
}
|
||||
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
&self.project
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
|
|
|
|||
|
|
@ -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<TaskStore>,
|
||||
candidates: Vec<(
|
||||
Option<TaskSourceKind>,
|
||||
Option<LanguageName>,
|
||||
DebugScenario,
|
||||
Option<DebugScenarioContext>,
|
||||
)>,
|
||||
|
|
@ -1005,28 +1006,89 @@ impl DebugDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_scenario_kind(
|
||||
fn get_task_subtitle(
|
||||
&self,
|
||||
task_kind: &Option<TaskSourceKind>,
|
||||
context: &Option<DebugScenarioContext>,
|
||||
cx: &mut App,
|
||||
) -> Option<String> {
|
||||
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<LanguageRegistry>,
|
||||
dap_registry: &DapRegistry,
|
||||
scenario: DebugScenario,
|
||||
) -> (Option<TaskSourceKind>, DebugScenario) {
|
||||
) -> (Option<LanguageName>, 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<picker::Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
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::<IconWithIndicator>(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<String> {
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::<crate::new_process_modal::NewProcessModal>(cx)
|
||||
workspace.active_modal::<NewProcessModal>(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::<NewProcessModal>(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);
|
||||
|
|
|
|||
|
|
@ -388,6 +388,7 @@ impl Inventory {
|
|||
.into_iter()
|
||||
.flat_map(|worktree| self.worktree_templates_from_settings(worktree))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
|
||||
name: language.name().into(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue