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:
parent
e4f8c4fb4c
commit
dd17fd3d5a
6 changed files with 171 additions and 21 deletions
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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_")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue