debug: Launch custom commands from start modal (#32484)

Release Notes:

- Add custom command launching from the `debug: start` modal

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
This commit is contained in:
Julia Ryan 2025-06-10 13:29:11 -07:00 committed by GitHub
parent e4f8c4fb4c
commit dd17fd3d5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 171 additions and 21 deletions

View file

@ -1478,7 +1478,8 @@
"Go": { "Go": {
"code_actions_on_format": { "code_actions_on_format": {
"source.organizeImports": true "source.organizeImports": true
} },
"debuggers": ["Delve"]
}, },
"GraphQL": { "GraphQL": {
"prettier": { "prettier": {
@ -1543,9 +1544,15 @@
"Plain Text": { "Plain Text": {
"allow_rewrap": "anywhere" "allow_rewrap": "anywhere"
}, },
"Python": {
"debuggers": ["Debugpy"]
},
"Ruby": { "Ruby": {
"language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."] "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
}, },
"Rust": {
"debuggers": ["CodeLLDB"]
},
"SCSS": { "SCSS": {
"prettier": { "prettier": {
"allowed": true "allowed": true

View file

@ -1,4 +1,4 @@
use collections::FxHashMap; use collections::{FxHashMap, HashMap};
use language::LanguageRegistry; use language::LanguageRegistry;
use paths::local_debug_file_relative_path; use paths::local_debug_file_relative_path;
use std::{ use std::{
@ -15,9 +15,9 @@ use dap::{
use editor::{Editor, EditorElement, EditorStyle}; use editor::{Editor, EditorElement, EditorStyle};
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription, HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
TextStyle, UnderlineStyle, WeakEntity, Subscription, TextStyle, UnderlineStyle, WeakEntity,
}; };
use itertools::Itertools as _; use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
@ -28,10 +28,10 @@ use theme::ThemeSettings;
use ui::{ use ui::{
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context, ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize, ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _, IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt, LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px, SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable,
relative, rems, v_flex, Tooltip, Window, div, h_flex, px, relative, rems, v_flex,
}; };
use util::ResultExt; use util::ResultExt;
use workspace::{ModalView, Workspace, pane}; use workspace::{ModalView, Workspace, pane};
@ -50,7 +50,7 @@ pub(super) struct NewProcessModal {
mode: NewProcessMode, mode: NewProcessMode,
debug_picker: Entity<Picker<DebugDelegate>>, debug_picker: Entity<Picker<DebugDelegate>>,
attach_mode: Entity<AttachMode>, attach_mode: Entity<AttachMode>,
launch_mode: Entity<ConfigureMode>, configure_mode: Entity<ConfigureMode>,
task_mode: TaskMode, task_mode: TaskMode,
debugger: Option<DebugAdapterName>, debugger: Option<DebugAdapterName>,
// save_scenario_state: Option<SaveScenarioState>, // save_scenario_state: Option<SaveScenarioState>,
@ -253,7 +253,7 @@ impl NewProcessModal {
Self { Self {
debug_picker, debug_picker,
attach_mode, attach_mode,
launch_mode: configure_mode, configure_mode,
task_mode, task_mode,
debugger: None, debugger: None,
mode, mode,
@ -283,7 +283,7 @@ impl NewProcessModal {
NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| { NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
this.clone().render(window, cx).into_any_element() 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() this.clone().render(dap_menu, window, cx).into_any_element()
}), }),
NewProcessMode::Debug => v_flex() NewProcessMode::Debug => v_flex()
@ -297,7 +297,7 @@ impl NewProcessModal {
match self.mode { match self.mode {
NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx), NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.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), NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
} }
} }
@ -305,7 +305,7 @@ impl NewProcessModal {
fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> { fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
let request = match self.mode { let request = match self.mode {
NewProcessMode::Launch => Some(DebugRequest::Launch( 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( NewProcessMode::Attach => Some(DebugRequest::Attach(
self.attach_mode.read(cx).debug_request(), self.attach_mode.read(cx).debug_request(),
@ -315,7 +315,7 @@ impl NewProcessModal {
let label = suggested_label(&request, debugger); let label = suggested_label(&request, debugger);
let stop_on_entry = if let NewProcessMode::Launch = &self.mode { 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 { } else {
None None
}; };
@ -831,7 +831,7 @@ impl Render for NewProcessModal {
.disabled( .disabled(
self.debugger.is_none() self.debugger.is_none()
|| self || self
.launch_mode .configure_mode
.read(cx) .read(cx)
.program .program
.read(cx) .read(cx)
@ -1202,7 +1202,7 @@ impl PickerDelegate for DebugDelegate {
} }
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> { fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
"".into() "Find a debug task, or debug a command.".into()
} }
fn update_matches( fn update_matches(
@ -1265,6 +1265,96 @@ impl PickerDelegate for DebugDelegate {
} }
} }
fn confirm_input(
&mut self,
_secondary: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
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::<Vec<_>>();
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<DebugAdapterName> =
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::<DapRegistry>()
.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<picker::Picker<Self>>) { fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
let debug_scenario = self let debug_scenario = self
.matches .matches
@ -1300,6 +1390,60 @@ impl PickerDelegate for DebugDelegate {
cx.emit(DismissEvent); cx.emit(DismissEvent);
} }
fn render_footer(
&self,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<ui::AnyElement> {
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( fn render_match(
&self, &self,
ix: usize, ix: usize,

View file

@ -75,6 +75,7 @@ impl DapLocator for CargoLocator {
} }
_ => {} _ => {}
} }
Some(DebugScenario { Some(DebugScenario {
adapter: adapter.0, adapter: adapter.0,
label: resolved_label.to_string().into(), label: resolved_label.to_string().into(),

View file

@ -98,7 +98,6 @@ impl DapLocator for GoLocator {
if build_config.command != "go" { if build_config.command != "go" {
return None; return None;
} }
let go_action = build_config.args.first()?; let go_action = build_config.args.first()?;
match go_action.as_str() { match go_action.as_str() {

View file

@ -31,8 +31,7 @@ impl DapLocator for NodeLocator {
if cfg!(not(debug_assertions)) { if cfg!(not(debug_assertions)) {
return None; return None;
} }
if adapter.0.as_ref() != "JavaScript" {
if adapter.as_ref() != "JavaScript" {
return None; return None;
} }
if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() { if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() {

View file

@ -22,7 +22,7 @@ impl DapLocator for PythonLocator {
resolved_label: &str, resolved_label: &str,
adapter: DebugAdapterName, adapter: DebugAdapterName,
) -> Option<DebugScenario> { ) -> Option<DebugScenario> {
if adapter.as_ref() != "Debugpy" { if adapter.0.as_ref() != "Debugpy" {
return None; return None;
} }
let valid_program = build_config.command.starts_with("$ZED_") let valid_program = build_config.command.starts_with("$ZED_")