From dd17fd3d5a7c22d04055e5055bff6ee0aa61a1a4 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Tue, 10 Jun 2025 13:29:11 -0700 Subject: [PATCH] debug: Launch custom commands from start modal (#32484) Release Notes: - Add custom command launching from the `debug: start` modal --------- Co-authored-by: Anthony Eid --- assets/settings/default.json | 9 +- crates/debugger_ui/src/new_process_modal.rs | 176 ++++++++++++++++-- crates/project/src/debugger/locators/cargo.rs | 1 + crates/project/src/debugger/locators/go.rs | 1 - crates/project/src/debugger/locators/node.rs | 3 +- .../project/src/debugger/locators/python.rs | 2 +- 6 files changed, 171 insertions(+), 21 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 4f04a7abdf..939f79e281 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1478,7 +1478,8 @@ "Go": { "code_actions_on_format": { "source.organizeImports": true - } + }, + "debuggers": ["Delve"] }, "GraphQL": { "prettier": { @@ -1543,9 +1544,15 @@ "Plain Text": { "allow_rewrap": "anywhere" }, + "Python": { + "debuggers": ["Debugpy"] + }, "Ruby": { "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."] }, + "Rust": { + "debuggers": ["CodeLLDB"] + }, "SCSS": { "prettier": { "allowed": true diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index b515bebd03..bc264d71ed 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1,4 +1,4 @@ -use collections::FxHashMap; +use collections::{FxHashMap, HashMap}; use language::LanguageRegistry; use paths::local_debug_file_relative_path; use std::{ @@ -15,9 +15,9 @@ use dap::{ use editor::{Editor, EditorElement, EditorStyle}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, - InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription, - TextStyle, UnderlineStyle, WeakEntity, + Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, + HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, + Subscription, TextStyle, UnderlineStyle, WeakEntity, }; use itertools::Itertools as _; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; @@ -28,10 +28,10 @@ use theme::ThemeSettings; use ui::{ ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize, - IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _, - ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt, - StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px, - relative, rems, v_flex, + IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label, + LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce, + SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable, + Tooltip, Window, div, h_flex, px, relative, rems, v_flex, }; use util::ResultExt; use workspace::{ModalView, Workspace, pane}; @@ -50,7 +50,7 @@ pub(super) struct NewProcessModal { mode: NewProcessMode, debug_picker: Entity>, attach_mode: Entity, - launch_mode: Entity, + configure_mode: Entity, task_mode: TaskMode, debugger: Option, // save_scenario_state: Option, @@ -253,7 +253,7 @@ impl NewProcessModal { Self { debug_picker, attach_mode, - launch_mode: configure_mode, + configure_mode, task_mode, debugger: None, mode, @@ -283,7 +283,7 @@ impl NewProcessModal { NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| { this.clone().render(window, cx).into_any_element() }), - NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| { + NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| { this.clone().render(dap_menu, window, cx).into_any_element() }), NewProcessMode::Debug => v_flex() @@ -297,7 +297,7 @@ impl NewProcessModal { match self.mode { NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx), NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx), - NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx), + NewProcessMode::Launch => self.configure_mode.read(cx).program.focus_handle(cx), NewProcessMode::Debug => self.debug_picker.focus_handle(cx), } } @@ -305,7 +305,7 @@ impl NewProcessModal { fn debug_scenario(&self, debugger: &str, cx: &App) -> Option { let request = match self.mode { NewProcessMode::Launch => Some(DebugRequest::Launch( - self.launch_mode.read(cx).debug_request(cx), + self.configure_mode.read(cx).debug_request(cx), )), NewProcessMode::Attach => Some(DebugRequest::Attach( self.attach_mode.read(cx).debug_request(), @@ -315,7 +315,7 @@ impl NewProcessModal { let label = suggested_label(&request, debugger); let stop_on_entry = if let NewProcessMode::Launch = &self.mode { - Some(self.launch_mode.read(cx).stop_on_entry.selected()) + Some(self.configure_mode.read(cx).stop_on_entry.selected()) } else { None }; @@ -831,7 +831,7 @@ impl Render for NewProcessModal { .disabled( self.debugger.is_none() || self - .launch_mode + .configure_mode .read(cx) .program .read(cx) @@ -1202,7 +1202,7 @@ impl PickerDelegate for DebugDelegate { } fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc { - "".into() + "Find a debug task, or debug a command.".into() } fn update_matches( @@ -1265,6 +1265,96 @@ impl PickerDelegate for DebugDelegate { } } + fn confirm_input( + &mut self, + _secondary: bool, + window: &mut Window, + cx: &mut Context>, + ) { + let text = self.prompt.clone(); + 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(); + + let mut args = shlex::split(&text).into_iter().flatten().peekable(); + let mut env = HashMap::default(); + while args.peek().is_some_and(|arg| arg.contains('=')) { + let arg = args.next().unwrap(); + let (lhs, rhs) = arg.split_once('=').unwrap(); + env.insert(lhs.to_string(), rhs.to_string()); + } + + let program = if let Some(program) = args.next() { + program + } else { + env = HashMap::default(); + text + }; + + let args = args.collect::>(); + let task = task::TaskTemplate { + label: "one-off".to_owned(), + env, + command: program, + args, + ..Default::default() + }; + + let Some(location) = self + .task_contexts + .as_ref() + .and_then(|cx| cx.location().cloned()) + else { + return; + }; + 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 Some(adapter): Option = + language::language_settings::language_settings(language_name, file, cx) + .debuggers + .first() + .map(SharedString::from) + .map(Into::into) + .or_else(|| { + language.and_then(|l| { + l.config() + .debuggers + .first() + .map(SharedString::from) + .map(Into::into) + }) + }) + else { + return; + }; + let Some(debug_scenario) = cx + .global::() + .locators() + .iter() + .find_map(|locator| locator.1.create_scenario(&task, "one-off", adapter.clone())) + else { + return; + }; + + send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx); + + self.debug_panel + .update(cx, |panel, cx| { + panel.start_session(debug_scenario, task_context, None, worktree_id, window, cx); + }) + .ok(); + + cx.emit(DismissEvent); + } + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { let debug_scenario = self .matches @@ -1300,6 +1390,60 @@ impl PickerDelegate for DebugDelegate { cx.emit(DismissEvent); } + fn render_footer( + &self, + window: &mut Window, + cx: &mut Context>, + ) -> Option { + let current_modifiers = window.modifiers(); + let footer = h_flex() + .w_full() + .h_8() + .p_2() + .justify_between() + .rounded_b_sm() + .bg(cx.theme().colors().ghost_element_selected) + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child( + // TODO: add button to open selected task in debug.json + h_flex().into_any_element(), + ) + .map(|this| { + if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() { + let action = picker::ConfirmInput { + secondary: current_modifiers.secondary(), + } + .boxed_clone(); + this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| { + Button::new("launch-custom", "Launch Custom") + .label_size(LabelSize::Small) + .key_binding(keybind) + .on_click(move |_, window, cx| { + window.dispatch_action(action.boxed_clone(), cx) + }) + })) + } else { + this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map( + |keybind| { + let is_recent_selected = + self.divider_index >= Some(self.selected_index); + let run_entry_label = + if is_recent_selected { "Rerun" } else { "Spawn" }; + + Button::new("spawn", run_entry_label) + .label_size(LabelSize::Small) + .key_binding(keybind) + .on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx); + }) + }, + )) + } + }); + Some(footer.into_any_element()) + } + fn render_match( &self, ix: usize, diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index 17df2c8c0e..5c43f02213 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -75,6 +75,7 @@ impl DapLocator for CargoLocator { } _ => {} } + Some(DebugScenario { adapter: adapter.0, label: resolved_label.to_string().into(), diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs index 1981b6be3c..e53cd7fbbd 100644 --- a/crates/project/src/debugger/locators/go.rs +++ b/crates/project/src/debugger/locators/go.rs @@ -98,7 +98,6 @@ impl DapLocator for GoLocator { if build_config.command != "go" { return None; } - let go_action = build_config.args.first()?; match go_action.as_str() { diff --git a/crates/project/src/debugger/locators/node.rs b/crates/project/src/debugger/locators/node.rs index a3a386ecda..407245b4ff 100644 --- a/crates/project/src/debugger/locators/node.rs +++ b/crates/project/src/debugger/locators/node.rs @@ -31,8 +31,7 @@ impl DapLocator for NodeLocator { if cfg!(not(debug_assertions)) { return None; } - - if adapter.as_ref() != "JavaScript" { + if adapter.0.as_ref() != "JavaScript" { return None; } if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() { diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs index 4af0d0b40d..c45cc3c11f 100644 --- a/crates/project/src/debugger/locators/python.rs +++ b/crates/project/src/debugger/locators/python.rs @@ -22,7 +22,7 @@ impl DapLocator for PythonLocator { resolved_label: &str, adapter: DebugAdapterName, ) -> Option { - if adapter.as_ref() != "Debugpy" { + if adapter.0.as_ref() != "Debugpy" { return None; } let valid_program = build_config.command.starts_with("$ZED_")