Edit debug tasks (#32908)
Release Notes: - Added the ability to edit LSP provided debug tasks --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
co-authored by
Conrad Irwin
parent
d549993c73
commit
a9107dfaeb
@@ -1,12 +1,10 @@
|
||||
use anyhow::bail;
|
||||
use anyhow::{Context as _, bail};
|
||||
use collections::{FxHashMap, HashMap};
|
||||
use language::LanguageRegistry;
|
||||
use paths::local_debug_file_relative_path;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
usize,
|
||||
};
|
||||
use tasks_ui::{TaskOverrides, TasksModal};
|
||||
@@ -18,35 +16,27 @@ use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
|
||||
Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
|
||||
KeyContext, Render, Subscription, Task, TextStyle, WeakEntity,
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{
|
||||
DebugScenarioContext, ProjectPath, TaskContexts, TaskSourceKind, task_store::TaskStore,
|
||||
};
|
||||
use settings::{Settings, initial_local_debug_tasks_content};
|
||||
use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use settings::Settings;
|
||||
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, CheckboxWithLabel, Clickable, Context, ContextMenu, Disableable, DropdownMenu,
|
||||
FluentBuilder, IconWithIndicator, Indicator, IntoElement, KeyBinding, ListItem,
|
||||
ListItemSpacing, ParentElement, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip,
|
||||
Window, div, prelude::*, px, relative, rems,
|
||||
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
|
||||
IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
|
||||
LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
|
||||
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div,
|
||||
h_flex, relative, rems, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace, pane};
|
||||
use workspace::{ModalView, Workspace, notifications::DetachAndPromptErr, pane};
|
||||
|
||||
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
|
||||
|
||||
#[allow(unused)]
|
||||
enum SaveScenarioState {
|
||||
Saving,
|
||||
Saved((ProjectPath, SharedString)),
|
||||
Failed(SharedString),
|
||||
}
|
||||
|
||||
pub(super) struct NewProcessModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
debug_panel: WeakEntity<DebugPanel>,
|
||||
@@ -56,7 +46,6 @@ pub(super) struct NewProcessModal {
|
||||
configure_mode: Entity<ConfigureMode>,
|
||||
task_mode: TaskMode,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
save_scenario_state: Option<SaveScenarioState>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
}
|
||||
|
||||
@@ -268,7 +257,6 @@ impl NewProcessModal {
|
||||
mode,
|
||||
debug_panel: debug_panel.downgrade(),
|
||||
workspace: workspace_handle,
|
||||
save_scenario_state: None,
|
||||
_subscriptions,
|
||||
}
|
||||
});
|
||||
@@ -420,63 +408,29 @@ impl NewProcessModal {
|
||||
self.debug_picker.read(cx).delegate.task_contexts.clone()
|
||||
}
|
||||
|
||||
fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task_contents = self.task_contexts(cx);
|
||||
pub fn save_debug_scenario(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task_contexts = self.task_contexts(cx);
|
||||
let Some(adapter) = self.debugger.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let scenario = self.debug_scenario(&adapter, cx);
|
||||
|
||||
self.save_scenario_state = Some(SaveScenarioState::Saving);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let Some((scenario, worktree_id)) = scenario
|
||||
.await
|
||||
.zip(task_contents.and_then(|tcx| tcx.worktree()))
|
||||
else {
|
||||
this.update(cx, |this, _| {
|
||||
this.save_scenario_state = Some(SaveScenarioState::Failed(
|
||||
"Couldn't get scenario or task contents".into(),
|
||||
))
|
||||
let scenario = scenario.await.context("no scenario to save")?;
|
||||
let worktree_id = task_contexts
|
||||
.context("no task contexts")?
|
||||
.worktree()
|
||||
.context("no active worktree")?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.debug_panel.update(cx, |panel, cx| {
|
||||
panel.save_scenario(scenario, worktree_id, window, cx)
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(save_scenario) = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.save_scenario(&scenario, worktree_id, window, cx)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let res = save_scenario.await;
|
||||
|
||||
this.update(cx, |this, _| match res {
|
||||
Ok(saved_file) => {
|
||||
this.save_scenario_state = Some(SaveScenarioState::Saved((
|
||||
saved_file,
|
||||
scenario.label.clone(),
|
||||
)))
|
||||
}
|
||||
Err(error) => {
|
||||
this.save_scenario_state =
|
||||
Some(SaveScenarioState::Failed(error.to_string().into()))
|
||||
}
|
||||
})??
|
||||
.await?;
|
||||
this.update_in(cx, |_, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
this.update(cx, |this, _| this.save_scenario_state.take())
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
.detach_and_prompt_err("Failed to edit debug.json", window, cx, |_, _, _| None);
|
||||
}
|
||||
|
||||
fn adapter_drop_down_menu(
|
||||
@@ -544,70 +498,6 @@ impl NewProcessModal {
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn open_debug_json(&self, window: &mut Window, cx: &mut Context<NewProcessModal>) {
|
||||
let this = cx.entity();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let worktree_id = this.update(cx, |this, cx| {
|
||||
let tcx = this.task_contexts(cx);
|
||||
tcx?.worktree()
|
||||
})?;
|
||||
|
||||
let Some(worktree_id) = worktree_id else {
|
||||
let _ = cx.prompt(
|
||||
PromptLevel::Critical,
|
||||
"Cannot open debug.json",
|
||||
Some("You must have at least one project open"),
|
||||
&[PromptButton::ok("Ok")],
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let editor = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_path(
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: local_debug_file_relative_path().into(),
|
||||
},
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})??
|
||||
.await?;
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
if let Some(editor) = editor.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
if let Some(singleton) = buffer.as_singleton() {
|
||||
singleton.update(cx, |buffer, cx| {
|
||||
if buffer.is_empty() {
|
||||
buffer.edit(
|
||||
[(0..0, initial_local_debug_tasks_content())],
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
||||
this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
|
||||
@@ -812,39 +702,21 @@ impl Render for NewProcessModal {
|
||||
NewProcessMode::Launch => el.child(
|
||||
container
|
||||
.child(
|
||||
h_flex()
|
||||
.text_ui_sm(cx)
|
||||
.text_color(Color::Muted.color(cx))
|
||||
.child(
|
||||
InteractiveText::new(
|
||||
"open-debug-json",
|
||||
StyledText::new(
|
||||
"Open .zed/debug.json for advanced configuration.",
|
||||
)
|
||||
.with_highlights([(
|
||||
5..20,
|
||||
HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: px(1.0),
|
||||
color: None,
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
)]),
|
||||
)
|
||||
.on_click(
|
||||
vec![5..20],
|
||||
{
|
||||
let this = cx.entity();
|
||||
move |_, window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.open_debug_json(window, cx);
|
||||
})
|
||||
}
|
||||
},
|
||||
h_flex().child(
|
||||
Button::new("edit-custom-debug", "Edit in debug.json")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.save_debug_scenario(window, cx);
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.configure_mode
|
||||
.read(cx)
|
||||
.program
|
||||
.read(cx)
|
||||
.is_empty(cx),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
@@ -862,29 +734,48 @@ impl Render for NewProcessModal {
|
||||
),
|
||||
),
|
||||
),
|
||||
NewProcessMode::Attach => el.child(
|
||||
NewProcessMode::Attach => el.child({
|
||||
let disabled = self.debugger.is_none()
|
||||
|| self
|
||||
.attach_mode
|
||||
.read(cx)
|
||||
.attach_picker
|
||||
.read(cx)
|
||||
.picker
|
||||
.read(cx)
|
||||
.delegate
|
||||
.match_count()
|
||||
== 0;
|
||||
let secondary_action = menu::SecondaryConfirm.boxed_clone();
|
||||
container
|
||||
.child(div().child(self.adapter_drop_down_menu(window, cx)))
|
||||
.child(div().children(
|
||||
KeyBinding::for_action(&*secondary_action, window, cx).map(
|
||||
|keybind| {
|
||||
Button::new("edit-attach-task", "Edit in debug.json")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(
|
||||
secondary_action.boxed_clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.disabled(disabled)
|
||||
},
|
||||
),
|
||||
))
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx)
|
||||
}))
|
||||
.disabled(
|
||||
self.debugger.is_none()
|
||||
|| self
|
||||
.attach_mode
|
||||
.read(cx)
|
||||
.attach_picker
|
||||
.read(cx)
|
||||
.picker
|
||||
.read(cx)
|
||||
.delegate
|
||||
.match_count()
|
||||
== 0,
|
||||
h_flex()
|
||||
.child(div().child(self.adapter_drop_down_menu(window, cx)))
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx)
|
||||
}))
|
||||
.disabled(disabled),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}),
|
||||
NewProcessMode::Debug => el,
|
||||
NewProcessMode::Task => el,
|
||||
}
|
||||
@@ -1048,25 +939,6 @@ impl ConfigureMode {
|
||||
)
|
||||
.checkbox_position(ui::IconPosition::End),
|
||||
)
|
||||
.child(
|
||||
CheckboxWithLabel::new(
|
||||
"debugger-save-to-debug-json",
|
||||
Label::new("Save to debug.json")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
self.save_to_debug_json,
|
||||
{
|
||||
let this = cx.weak_entity();
|
||||
move |state, _, cx| {
|
||||
this.update(cx, |this, _| {
|
||||
this.save_to_debug_json = *state;
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
},
|
||||
)
|
||||
.checkbox_position(ui::IconPosition::End),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1329,12 +1201,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_input(
|
||||
&mut self,
|
||||
_secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
fn confirm_input(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
let text = self.prompt.clone();
|
||||
let (task_context, worktree_id) = self
|
||||
.task_contexts
|
||||
@@ -1364,7 +1231,7 @@ impl PickerDelegate for DebugDelegate {
|
||||
|
||||
let args = args.collect::<Vec<_>>();
|
||||
let task = task::TaskTemplate {
|
||||
label: "one-off".to_owned(),
|
||||
label: "one-off".to_owned(), // TODO: rename using command as label
|
||||
env,
|
||||
command: program,
|
||||
args,
|
||||
@@ -1405,7 +1272,11 @@ impl PickerDelegate for DebugDelegate {
|
||||
.background_spawn(async move {
|
||||
for locator in locators {
|
||||
if let Some(scenario) =
|
||||
locator.1.create_scenario(&task, "one-off", &adapter).await
|
||||
// TODO: use a more informative label than "one-off"
|
||||
locator
|
||||
.1
|
||||
.create_scenario(&task, &task.label, &adapter)
|
||||
.await
|
||||
{
|
||||
return Some(scenario);
|
||||
}
|
||||
@@ -1439,13 +1310,18 @@ impl PickerDelegate for DebugDelegate {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
|
||||
fn confirm(
|
||||
&mut self,
|
||||
secondary: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<picker::Picker<Self>>,
|
||||
) {
|
||||
let debug_scenario = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
.and_then(|match_candidate| self.candidates.get(match_candidate.candidate_id).cloned());
|
||||
|
||||
let Some((_, debug_scenario, context)) = debug_scenario else {
|
||||
let Some((kind, debug_scenario, context)) = debug_scenario else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -1463,24 +1339,38 @@ impl PickerDelegate for DebugDelegate {
|
||||
});
|
||||
let DebugScenarioContext {
|
||||
task_context,
|
||||
active_buffer,
|
||||
active_buffer: _,
|
||||
worktree_id,
|
||||
} = context;
|
||||
let active_buffer = active_buffer.and_then(|buffer| buffer.upgrade());
|
||||
|
||||
send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
|
||||
self.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.start_session(
|
||||
debug_scenario,
|
||||
task_context,
|
||||
active_buffer,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if secondary {
|
||||
let Some(kind) = kind else { return };
|
||||
let Some(id) = worktree_id else { return };
|
||||
let debug_panel = self.debug_panel.clone();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
debug_panel
|
||||
.update_in(cx, |debug_panel, window, cx| {
|
||||
debug_panel.go_to_scenario_definition(kind, debug_scenario, id, window, cx)
|
||||
})?
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.ok();
|
||||
.detach();
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
@@ -1498,19 +1388,23 @@ impl PickerDelegate for DebugDelegate {
|
||||
let footer = h_flex()
|
||||
.w_full()
|
||||
.p_1p5()
|
||||
.justify_end()
|
||||
.justify_between()
|
||||
.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(),
|
||||
// )
|
||||
.children({
|
||||
let action = menu::SecondaryConfirm.boxed_clone();
|
||||
KeyBinding::for_action(&*action, window, cx).map(|keybind| {
|
||||
Button::new("edit-debug-task", "Edit in debug.json")
|
||||
.label_size(LabelSize::Small)
|
||||
.key_binding(keybind)
|
||||
.on_click(move |_, window, cx| {
|
||||
window.dispatch_action(action.boxed_clone(), cx)
|
||||
})
|
||||
})
|
||||
})
|
||||
.map(|this| {
|
||||
if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
|
||||
let action = picker::ConfirmInput {
|
||||
secondary: current_modifiers.secondary(),
|
||||
}
|
||||
.boxed_clone();
|
||||
let action = picker::ConfirmInput { secondary: false }.boxed_clone();
|
||||
this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
|
||||
Button::new("launch-custom", "Launch Custom")
|
||||
.key_binding(keybind)
|
||||
@@ -1607,3 +1501,35 @@ pub(crate) fn resolve_path(path: &mut String) {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl NewProcessModal {
|
||||
pub(crate) fn set_configure(
|
||||
&mut self,
|
||||
program: impl AsRef<str>,
|
||||
cwd: impl AsRef<str>,
|
||||
stop_on_entry: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.mode = NewProcessMode::Launch;
|
||||
self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
|
||||
|
||||
self.configure_mode.update(cx, |configure, cx| {
|
||||
configure.program.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.set_text(program.as_ref(), window, cx);
|
||||
});
|
||||
|
||||
configure.cwd.update(cx, |editor, cx| {
|
||||
editor.clear(window, cx);
|
||||
editor.set_text(cwd.as_ref(), window, cx);
|
||||
});
|
||||
|
||||
configure.stop_on_entry = match stop_on_entry {
|
||||
true => ToggleState::Selected,
|
||||
_ => ToggleState::Unselected,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user