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": {
|
||||
"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
|
||||
|
|
|
@ -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<Picker<DebugDelegate>>,
|
||||
attach_mode: Entity<AttachMode>,
|
||||
launch_mode: Entity<ConfigureMode>,
|
||||
configure_mode: Entity<ConfigureMode>,
|
||||
task_mode: TaskMode,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
// save_scenario_state: Option<SaveScenarioState>,
|
||||
|
@ -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<DebugScenario> {
|
||||
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<str> {
|
||||
"".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<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>>) {
|
||||
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<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(
|
||||
&self,
|
||||
ix: usize,
|
||||
|
|
|
@ -75,6 +75,7 @@ impl DapLocator for CargoLocator {
|
|||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Some(DebugScenario {
|
||||
adapter: adapter.0,
|
||||
label: resolved_label.to_string().into(),
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -22,7 +22,7 @@ impl DapLocator for PythonLocator {
|
|||
resolved_label: &str,
|
||||
adapter: DebugAdapterName,
|
||||
) -> Option<DebugScenario> {
|
||||
if adapter.as_ref() != "Debugpy" {
|
||||
if adapter.0.as_ref() != "Debugpy" {
|
||||
return None;
|
||||
}
|
||||
let valid_program = build_config.command.starts_with("$ZED_")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue