Cargo.lock 🔗
@@ -4199,6 +4199,7 @@ dependencies = [
"log",
"menu",
"parking_lot",
+ "paths",
"picker",
"pretty_assertions",
"project",
Anthony Eid created
This PR simplifies the new session modal by flattening its three modes
and updating the UI to be less noisy. The new UI also defaults to the
Debug Scenario Picker, and allows users to save debug scenarios created
in the UI to the active worktree's .zed/debug.json file.
Release Notes:
- N/A
Cargo.lock | 1
crates/debugger_ui/Cargo.toml | 1
crates/debugger_ui/src/attach_modal.rs | 8
crates/debugger_ui/src/debugger_panel.rs | 71 +
crates/debugger_ui/src/debugger_ui.rs | 31
crates/debugger_ui/src/new_session_modal.rs | 803 +++++++++------------
crates/debugger_ui/src/session/running.rs | 2
crates/debugger_ui/src/tests/attach_modal.rs | 2
crates/languages/src/json.rs | 2
crates/paths/src/paths.rs | 6
crates/task/src/debug_format.rs | 8
crates/zed/src/zed.rs | 2
12 files changed, 447 insertions(+), 490 deletions(-)
@@ -4199,6 +4199,7 @@ dependencies = [
"log",
"menu",
"parking_lot",
+ "paths",
"picker",
"pretty_assertions",
"project",
@@ -43,6 +43,7 @@ language.workspace = true
log.workspace = true
menu.workspace = true
parking_lot.workspace = true
+paths.workspace = true
picker.workspace = true
pretty_assertions.workspace = true
project.workspace = true
@@ -32,12 +32,12 @@ pub(crate) struct AttachModalDelegate {
impl AttachModalDelegate {
fn new(
- workspace: Entity<Workspace>,
+ workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition,
candidates: Arc<[Candidate]>,
) -> Self {
Self {
- workspace: workspace.downgrade(),
+ workspace,
definition,
candidates,
selected_index: 0,
@@ -55,7 +55,7 @@ pub struct AttachModal {
impl AttachModal {
pub fn new(
definition: DebugTaskDefinition,
- workspace: Entity<Workspace>,
+ workspace: WeakEntity<Workspace>,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
@@ -82,7 +82,7 @@ impl AttachModal {
}
pub(super) fn with_processes(
- workspace: Entity<Workspace>,
+ workspace: WeakEntity<Workspace>,
definition: DebugTaskDefinition,
processes: Arc<[Candidate]>,
modal: bool,
@@ -5,15 +5,15 @@ use crate::{
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, StepBack,
StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
};
-use anyhow::Result;
+use anyhow::{Result, anyhow};
use command_palette_hooks::CommandPaletteFilter;
+use dap::StartDebuggingRequestArguments;
use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition;
use dap::{
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
client::SessionId, debugger_settings::DebuggerSettings,
};
-use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
use gpui::{
Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
@@ -54,12 +54,11 @@ pub enum DebugPanelEvent {
}
actions!(debug_panel, [ToggleFocus]);
+
pub struct DebugPanel {
size: Pixels,
sessions: Vec<Entity<DebugSession>>,
active_session: Option<Entity<DebugSession>>,
- /// This represents the last debug definition that was created in the new session modal
- pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
@@ -80,7 +79,6 @@ impl DebugPanel {
size: px(300.),
sessions: vec![],
active_session: None,
- past_debug_definition: None,
focus_handle: cx.focus_handle(),
project,
workspace: workspace.weak_handle(),
@@ -992,6 +990,69 @@ impl DebugPanel {
self.active_session = Some(session_item);
cx.notify();
}
+
+ pub(crate) fn save_scenario(
+ &self,
+ scenario: &DebugScenario,
+ worktree_id: WorktreeId,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Task<Result<()>> {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
+ return Task::ready(Err(anyhow!("Couldn't get worktree path")));
+ };
+
+ let serialized_scenario = serde_json::to_value(scenario);
+
+ path.push(paths::local_debug_file_relative_path());
+
+ cx.spawn_in(window, async move |workspace, cx| {
+ let serialized_scenario = serialized_scenario?;
+ let path = path.as_path();
+ let fs =
+ workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
+
+ if !fs.is_file(path).await {
+ let content =
+ serde_json::to_string_pretty(&serde_json::Value::Array(vec![
+ serialized_scenario,
+ ]))?;
+
+ fs.create_file(path, Default::default()).await?;
+ fs.save(path, &content.into(), Default::default()).await?;
+ } else {
+ let content = fs.load(path).await?;
+ let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
+ values.push(serialized_scenario);
+ fs.save(
+ path,
+ &serde_json::to_string_pretty(&values).map(Into::into)?,
+ Default::default(),
+ )
+ .await?;
+ }
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ if let Some(project_path) = workspace
+ .project()
+ .read(cx)
+ .project_path_for_absolute_path(&path, cx)
+ {
+ workspace.open_path(project_path, None, true, window, cx)
+ } else {
+ Task::ready(Err(anyhow!(
+ "Couldn't get project path for .zed/debug.json in active worktree"
+ )))
+ }
+ })?.await?;
+
+ anyhow::Ok(())
+ })
+ })
+ .unwrap_or_else(|err| Task::ready(Err(err)))
+ }
}
impl EventEmitter<PanelEvent> for DebugPanel {}
@@ -147,36 +147,7 @@ pub fn init(cx: &mut App) {
},
)
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
- if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
- let weak_panel = debug_panel.downgrade();
- let weak_workspace = cx.weak_entity();
- let task_store = workspace.project().read(cx).task_store().clone();
-
- cx.spawn_in(window, async move |this, cx| {
- let task_contexts = this
- .update_in(cx, |workspace, window, cx| {
- tasks_ui::task_contexts(workspace, window, cx)
- })?
- .await;
-
- this.update_in(cx, |workspace, window, cx| {
- workspace.toggle_modal(window, cx, |window, cx| {
- NewSessionModal::new(
- debug_panel.read(cx).past_debug_definition.clone(),
- weak_panel,
- weak_workspace,
- Some(task_store),
- task_contexts,
- window,
- cx,
- )
- });
- })?;
-
- anyhow::Ok(())
- })
- .detach()
- }
+ NewSessionModal::show(workspace, window, cx);
});
})
})
@@ -29,7 +29,7 @@ use ui::{
relative, rems, v_flex,
};
use util::ResultExt;
-use workspace::{ModalView, Workspace};
+use workspace::{ModalView, Workspace, pane};
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
@@ -37,11 +37,12 @@ pub(super) struct NewSessionModal {
workspace: WeakEntity<Workspace>,
debug_panel: WeakEntity<DebugPanel>,
mode: NewSessionMode,
- stop_on_entry: ToggleState,
- initialize_args: Option<serde_json::Value>,
+ launch_picker: Entity<Picker<DebugScenarioDelegate>>,
+ attach_mode: Entity<AttachMode>,
+ custom_mode: Entity<CustomMode>,
debugger: Option<DebugAdapterName>,
- last_selected_profile_name: Option<SharedString>,
task_contexts: Arc<TaskContexts>,
+ _subscriptions: [Subscription; 2],
}
fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
@@ -63,67 +64,126 @@ fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
}
impl NewSessionModal {
- pub(super) fn new(
- past_debug_definition: Option<DebugTaskDefinition>,
- debug_panel: WeakEntity<DebugPanel>,
- workspace: WeakEntity<Workspace>,
- task_store: Option<Entity<TaskStore>>,
- task_contexts: TaskContexts,
+ pub(super) fn show(
+ workspace: &mut Workspace,
window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let debugger = past_debug_definition
- .as_ref()
- .map(|def| def.adapter.clone());
+ cx: &mut Context<Workspace>,
+ ) {
+ let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
+ return;
+ };
+ let task_store = workspace.project().read(cx).task_store().clone();
- let stop_on_entry = past_debug_definition
- .as_ref()
- .and_then(|def| def.stop_on_entry);
+ cx.spawn_in(window, async move |workspace, cx| {
+ let task_contexts = Arc::from(
+ workspace
+ .update_in(cx, |workspace, window, cx| {
+ tasks_ui::task_contexts(workspace, window, cx)
+ })?
+ .await,
+ );
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ let workspace_handle = workspace.weak_handle();
+ workspace.toggle_modal(window, cx, |window, cx| {
+ let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
+
+ let launch_picker = cx.new(|cx| {
+ Picker::uniform_list(
+ DebugScenarioDelegate::new(
+ debug_panel.downgrade(),
+ workspace_handle.clone(),
+ task_store,
+ task_contexts.clone(),
+ ),
+ window,
+ cx,
+ )
+ .modal(false)
+ });
- let launch_config = match past_debug_definition.map(|def| def.request) {
- Some(DebugRequest::Launch(launch_config)) => Some(launch_config),
- _ => None,
- };
+ let _subscriptions = [
+ cx.subscribe(&launch_picker, |_, _, _, cx| {
+ cx.emit(DismissEvent);
+ }),
+ cx.subscribe(
+ &attach_mode.read(cx).attach_picker.clone(),
+ |_, _, _, cx| {
+ cx.emit(DismissEvent);
+ },
+ ),
+ ];
+
+ let custom_mode = CustomMode::new(None, window, cx);
+
+ Self {
+ launch_picker,
+ attach_mode,
+ custom_mode,
+ debugger: None,
+ mode: NewSessionMode::Launch,
+ debug_panel: debug_panel.downgrade(),
+ workspace: workspace_handle,
+ task_contexts,
+ _subscriptions,
+ }
+ });
+ })?;
- if let Some(task_store) = task_store {
- cx.defer_in(window, |this, window, cx| {
- this.mode = NewSessionMode::scenario(
- this.debug_panel.clone(),
- this.workspace.clone(),
- task_store,
- window,
- cx,
- );
- });
- };
+ anyhow::Ok(())
+ })
+ .detach();
+ }
- Self {
- workspace: workspace.clone(),
- debugger,
- debug_panel,
- mode: NewSessionMode::launch(launch_config, window, cx),
- stop_on_entry: stop_on_entry
- .map(Into::into)
- .unwrap_or(ToggleState::Unselected),
- last_selected_profile_name: None,
- initialize_args: None,
- task_contexts: Arc::new(task_contexts),
+ fn render_mode(&self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+ let dap_menu = self.adapter_drop_down_menu(window, cx);
+ match self.mode {
+ NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
+ this.clone().render(window, cx).into_any_element()
+ }),
+ NewSessionMode::Custom => self.custom_mode.update(cx, |this, cx| {
+ this.clone().render(dap_menu, window, cx).into_any_element()
+ }),
+ NewSessionMode::Launch => v_flex()
+ .w(rems(34.))
+ .child(self.launch_picker.clone())
+ .into_any_element(),
+ }
+ }
+
+ fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
+ match self.mode {
+ NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
+ NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx),
+ NewSessionMode::Launch => self.launch_picker.focus_handle(cx),
}
}
- fn debug_config(&self, cx: &App, debugger: &str) -> Option<DebugScenario> {
- let request = self.mode.debug_task(cx)?;
+ fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
+ let request = match self.mode {
+ NewSessionMode::Custom => Some(DebugRequest::Launch(
+ self.custom_mode.read(cx).debug_request(cx),
+ )),
+ NewSessionMode::Attach => Some(DebugRequest::Attach(
+ self.attach_mode.read(cx).debug_request(),
+ )),
+ _ => None,
+ }?;
let label = suggested_label(&request, debugger);
+
+ let stop_on_entry = if let NewSessionMode::Custom = &self.mode {
+ Some(self.custom_mode.read(cx).stop_on_entry.selected())
+ } else {
+ None
+ };
+
Some(DebugScenario {
adapter: debugger.to_owned().into(),
label,
request: Some(request),
- initialize_args: self.initialize_args.clone(),
+ initialize_args: None,
tcp_connection: None,
- stop_on_entry: match self.stop_on_entry {
- ToggleState::Selected => Some(true),
- _ => None,
- },
+ stop_on_entry,
build: None,
})
}
@@ -135,14 +195,14 @@ impl NewSessionModal {
return;
};
- if let NewSessionMode::Scenario(picker) = &self.mode {
- picker.update(cx, |picker, cx| {
+ if let NewSessionMode::Launch = &self.mode {
+ self.launch_picker.update(cx, |picker, cx| {
picker.delegate.confirm(false, window, cx);
});
return;
}
- let Some(config) = self.debug_config(cx, debugger) else {
+ let Some(config) = self.debug_scenario(debugger, cx) else {
log::error!("debug config not found in mode: {}", self.mode);
return;
};
@@ -189,7 +249,7 @@ impl NewSessionModal {
&self,
window: &mut Window,
cx: &mut Context<Self>,
- ) -> Option<ui::DropdownMenu> {
+ ) -> ui::DropdownMenu {
let workspace = self.workspace.clone();
let weak = cx.weak_entity();
let label = self
@@ -207,6 +267,7 @@ impl NewSessionModal {
.and_then(|location| location.buffer.read(cx).language())
})
.cloned();
+
DropdownMenu::new(
"dap-adapter-picker",
label,
@@ -217,8 +278,8 @@ impl NewSessionModal {
weak.update(cx, |this, cx| {
this.debugger = Some(name.clone());
cx.notify();
- if let NewSessionMode::Attach(attach) = &this.mode {
- Self::update_attach_picker(&attach, &name, window, cx);
+ if let NewSessionMode::Attach = &this.mode {
+ Self::update_attach_picker(&this.attach_mode, &name, window, cx);
}
})
.ok();
@@ -227,7 +288,6 @@ impl NewSessionModal {
let mut available_adapters = workspace
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
- .ok()
.unwrap_or_default();
if let Some(language) = active_buffer_language {
available_adapters.sort_by_key(|adapter| {
@@ -245,195 +305,24 @@ impl NewSessionModal {
menu
}),
)
- .into()
- }
-
- fn debug_config_drop_down_menu(
- &self,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> ui::DropdownMenu {
- let workspace = self.workspace.clone();
- let weak = cx.weak_entity();
- let last_profile = self.last_selected_profile_name.clone();
- let worktree = workspace
- .update(cx, |this, cx| {
- this.project().read(cx).visible_worktrees(cx).next()
- })
- .unwrap_or_default();
- DropdownMenu::new(
- "debug-config-menu",
- last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()),
- ContextMenu::build(window, cx, move |mut menu, _, cx| {
- let setter_for_name = |task: DebugScenario| {
- let weak = weak.clone();
- move |window: &mut Window, cx: &mut App| {
- weak.update(cx, |this, cx| {
- this.last_selected_profile_name = Some(SharedString::from(&task.label));
- this.debugger = Some(DebugAdapterName(task.adapter.clone()));
- this.initialize_args = task.initialize_args.clone();
- match &task.request {
- Some(DebugRequest::Launch(launch_config)) => {
- this.mode = NewSessionMode::launch(
- Some(launch_config.clone()),
- window,
- cx,
- );
- }
- Some(DebugRequest::Attach(_)) => {
- let Some(workspace) = this.workspace.upgrade() else {
- return;
- };
- this.mode = NewSessionMode::attach(
- this.debugger.clone(),
- workspace,
- window,
- cx,
- );
- this.mode.focus_handle(cx).focus(window);
- if let Some((debugger, attach)) =
- this.debugger.as_ref().zip(this.mode.as_attach())
- {
- Self::update_attach_picker(&attach, &debugger, window, cx);
- }
- }
- _ => log::warn!("Selected debug scenario without either attach or launch request specified"),
- }
- cx.notify();
- })
- .ok();
- }
- };
-
- let available_tasks: Vec<DebugScenario> = workspace
- .update(cx, |this, cx| {
- this.project()
- .read(cx)
- .task_store()
- .read(cx)
- .task_inventory()
- .iter()
- .flat_map(|task_inventory| {
- task_inventory.read(cx).list_debug_scenarios(
- worktree
- .as_ref()
- .map(|worktree| worktree.read(cx).id())
- .iter()
- .copied(),
- )
- })
- .map(|(_source_kind, scenario)| scenario)
- .collect()
- })
- .ok()
- .unwrap_or_default();
-
- for debug_definition in available_tasks {
- menu = menu.entry(
- debug_definition.label.clone(),
- None,
- setter_for_name(debug_definition),
- );
- }
- menu
- }),
- )
}
}
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
-static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
#[derive(Clone)]
enum NewSessionMode {
- Launch(Entity<LaunchMode>),
- Scenario(Entity<Picker<DebugScenarioDelegate>>),
- Attach(Entity<AttachMode>),
-}
-
-impl NewSessionMode {
- fn debug_task(&self, cx: &App) -> Option<DebugRequest> {
- match self {
- NewSessionMode::Launch(entity) => Some(entity.read(cx).debug_task(cx).into()),
- NewSessionMode::Attach(entity) => Some(entity.read(cx).debug_task().into()),
- NewSessionMode::Scenario(_) => None,
- }
- }
- fn as_attach(&self) -> Option<&Entity<AttachMode>> {
- if let NewSessionMode::Attach(entity) = self {
- Some(entity)
- } else {
- None
- }
- }
-
- fn scenario(
- debug_panel: WeakEntity<DebugPanel>,
- workspace: WeakEntity<Workspace>,
- task_store: Entity<TaskStore>,
- window: &mut Window,
- cx: &mut Context<NewSessionModal>,
- ) -> NewSessionMode {
- let picker = cx.new(|cx| {
- Picker::uniform_list(
- DebugScenarioDelegate::new(debug_panel, workspace, task_store),
- window,
- cx,
- )
- .modal(false)
- });
-
- cx.subscribe(&picker, |_, _, _, cx| {
- cx.emit(DismissEvent);
- })
- .detach();
-
- picker.focus_handle(cx).focus(window);
- NewSessionMode::Scenario(picker)
- }
-
- fn attach(
- debugger: Option<DebugAdapterName>,
- workspace: Entity<Workspace>,
- window: &mut Window,
- cx: &mut Context<NewSessionModal>,
- ) -> Self {
- Self::Attach(AttachMode::new(debugger, workspace, window, cx))
- }
-
- fn launch(
- past_launch_config: Option<LaunchRequest>,
- window: &mut Window,
- cx: &mut Context<NewSessionModal>,
- ) -> Self {
- Self::Launch(LaunchMode::new(past_launch_config, window, cx))
- }
-
- fn has_match(&self, cx: &App) -> bool {
- match self {
- NewSessionMode::Scenario(picker) => picker.read(cx).delegate.match_count() > 0,
- NewSessionMode::Attach(picker) => {
- picker
- .read(cx)
- .attach_picker
- .read(cx)
- .picker
- .read(cx)
- .delegate
- .match_count()
- > 0
- }
- _ => false,
- }
- }
+ Custom,
+ Attach,
+ Launch,
}
impl std::fmt::Display for NewSessionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mode = match self {
- NewSessionMode::Launch(_) => "launch".to_owned(),
- NewSessionMode::Attach(_) => "attach".to_owned(),
- NewSessionMode::Scenario(_) => "scenario picker".to_owned(),
+ NewSessionMode::Launch => "Launch".to_owned(),
+ NewSessionMode::Attach => "Attach".to_owned(),
+ NewSessionMode::Custom => "Custom".to_owned(),
};
write!(f, "{}", mode)
@@ -442,28 +331,7 @@ impl std::fmt::Display for NewSessionMode {
impl Focusable for NewSessionMode {
fn focus_handle(&self, cx: &App) -> FocusHandle {
- match &self {
- NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
- NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx),
- NewSessionMode::Scenario(entity) => entity.read(cx).focus_handle(cx),
- }
- }
-}
-
-impl RenderOnce for NewSessionMode {
- fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
- match self {
- NewSessionMode::Launch(entity) => entity.update(cx, |this, cx| {
- this.clone().render(window, cx).into_any_element()
- }),
- NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
- this.clone().render(window, cx).into_any_element()
- }),
- NewSessionMode::Scenario(entity) => v_flex()
- .w(rems(34.))
- .child(entity.clone())
- .into_any_element(),
- }
+ cx.focus_handle()
}
}
@@ -514,11 +382,36 @@ impl Render for NewSessionModal {
v_flex()
.size_full()
.w(rems(34.))
+ .key_context("Pane")
.elevation_3(cx)
.bg(cx.theme().colors().elevated_surface_background)
.on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
cx.emit(DismissEvent);
}))
+ .on_action(
+ cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
+ this.mode = match this.mode {
+ NewSessionMode::Attach => NewSessionMode::Launch,
+ NewSessionMode::Launch => NewSessionMode::Attach,
+ _ => {
+ return;
+ }
+ };
+
+ this.mode_focus_handle(cx).focus(window);
+ }),
+ )
+ .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
+ this.mode = match this.mode {
+ NewSessionMode::Attach => NewSessionMode::Launch,
+ NewSessionMode::Launch => NewSessionMode::Attach,
+ _ => {
+ return;
+ }
+ };
+
+ this.mode_focus_handle(cx).focus(window);
+ }))
.child(
h_flex()
.w_full()
@@ -529,84 +422,44 @@ impl Render for NewSessionModal {
.justify_start()
.w_full()
.child(
- ToggleButton::new("debugger-session-ui-picker-button", "Scenarios")
+ ToggleButton::new("debugger-session-ui-picker-button", "Launch")
.size(ButtonSize::Default)
.style(ui::ButtonStyle::Subtle)
- .toggle_state(matches!(self.mode, NewSessionMode::Scenario(_)))
+ .toggle_state(matches!(self.mode, NewSessionMode::Launch))
.on_click(cx.listener(|this, _, window, cx| {
- let Some(task_store) = this
- .workspace
- .update(cx, |workspace, cx| {
- workspace.project().read(cx).task_store().clone()
- })
- .ok()
- else {
- return;
- };
-
- this.mode = NewSessionMode::scenario(
- this.debug_panel.clone(),
- this.workspace.clone(),
- task_store,
- window,
- cx,
- );
-
+ this.mode = NewSessionMode::Launch;
+ this.mode_focus_handle(cx).focus(window);
cx.notify();
}))
.first(),
)
.child(
- ToggleButton::new(
- "debugger-session-ui-launch-button",
- "New Session",
- )
- .size(ButtonSize::Default)
- .style(ui::ButtonStyle::Subtle)
- .toggle_state(matches!(self.mode, NewSessionMode::Launch(_)))
- .on_click(cx.listener(|this, _, window, cx| {
- this.mode = NewSessionMode::launch(None, window, cx);
- this.mode.focus_handle(cx).focus(window);
- cx.notify();
- }))
- .middle(),
- )
- .child(
- ToggleButton::new(
- "debugger-session-ui-attach-button",
- "Attach to Process",
- )
- .size(ButtonSize::Default)
- .toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
- .style(ui::ButtonStyle::Subtle)
- .on_click(cx.listener(|this, _, window, cx| {
- let Some(workspace) = this.workspace.upgrade() else {
- return;
- };
- this.mode = NewSessionMode::attach(
- this.debugger.clone(),
- workspace,
- window,
- cx,
- );
- this.mode.focus_handle(cx).focus(window);
- if let Some((debugger, attach)) =
- this.debugger.as_ref().zip(this.mode.as_attach())
- {
- Self::update_attach_picker(&attach, &debugger, window, cx);
- }
-
- cx.notify();
- }))
- .last(),
+ ToggleButton::new("debugger-session-ui-attach-button", "Attach")
+ .size(ButtonSize::Default)
+ .toggle_state(matches!(self.mode, NewSessionMode::Attach))
+ .style(ui::ButtonStyle::Subtle)
+ .on_click(cx.listener(|this, _, window, cx| {
+ this.mode = NewSessionMode::Attach;
+
+ if let Some(debugger) = this.debugger.as_ref() {
+ Self::update_attach_picker(
+ &this.attach_mode,
+ &debugger,
+ window,
+ cx,
+ );
+ }
+ this.mode_focus_handle(cx).focus(window);
+ cx.notify();
+ }))
+ .last(),
),
)
.justify_between()
- .children(self.adapter_drop_down_menu(window, cx))
.border_color(cx.theme().colors().border_variant)
.border_b_1(),
)
- .child(v_flex().child(self.mode.clone().render(window, cx)))
+ .child(v_flex().child(self.render_mode(window, cx)))
.child(
h_flex()
.justify_between()
@@ -615,53 +468,91 @@ impl Render for NewSessionModal {
.border_color(cx.theme().colors().border_variant)
.border_t_1()
.w_full()
- .child(
- matches!(self.mode, NewSessionMode::Scenario(_))
- .not()
- .then(|| {
- self.debug_config_drop_down_menu(window, cx)
- .into_any_element()
- })
- .unwrap_or_else(|| v_flex().w_full().into_any_element()),
- )
- .child(
- h_flex()
- .justify_end()
- .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
- let weak = cx.weak_entity();
- this.child(
- CheckboxWithLabel::new(
- "debugger-stop-on-entry",
- Label::new("Stop on Entry").size(ui::LabelSize::Small),
- self.stop_on_entry,
- move |state, _, cx| {
- weak.update(cx, |this, _| {
- this.stop_on_entry = *state;
- })
- .ok();
- },
- )
- .checkbox_position(ui::IconPosition::End),
- )
- })
- .child(
- Button::new("debugger-spawn", "Start")
- .on_click(cx.listener(|this, _, window, cx| match &this.mode {
- NewSessionMode::Scenario(picker) => {
- picker.update(cx, |picker, cx| {
- picker.delegate.confirm(true, window, cx)
- })
- }
- _ => this.start_new_session(window, cx),
- }))
- .disabled(match self.mode {
- NewSessionMode::Scenario(_) => !self.mode.has_match(cx),
- NewSessionMode::Attach(_) => {
- self.debugger.is_none() || !self.mode.has_match(cx)
+ .child(match self.mode {
+ NewSessionMode::Attach => {
+ div().child(self.adapter_drop_down_menu(window, cx))
+ }
+ NewSessionMode::Launch => div().child(
+ Button::new("new-session-modal-custom", "Custom").on_click({
+ let this = cx.weak_entity();
+ move |_, window, cx| {
+ this.update(cx, |this, cx| {
+ this.mode = NewSessionMode::Custom;
+ this.mode_focus_handle(cx).focus(window);
+ })
+ .ok();
+ }
+ }),
+ ),
+ NewSessionMode::Custom => div().child(
+ Button::new("new-session-modal-back", "Save to .zed/debug.json...")
+ .on_click(cx.listener(|this, _, window, cx| {
+ let Some(save_scenario_task) = this
+ .debugger
+ .as_ref()
+ .and_then(|debugger| this.debug_scenario(&debugger, cx))
+ .zip(this.task_contexts.worktree())
+ .and_then(|(scenario, worktree_id)| {
+ this.debug_panel
+ .update(cx, |panel, cx| {
+ panel.save_scenario(
+ &scenario,
+ worktree_id,
+ window,
+ cx,
+ )
+ })
+ .ok()
+ })
+ else {
+ return;
+ };
+
+ cx.spawn(async move |this, cx| {
+ if save_scenario_task.await.is_ok() {
+ this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
}
- NewSessionMode::Launch(_) => self.debugger.is_none(),
- }),
- ),
+ })
+ .detach();
+ }))
+ .disabled(
+ self.debugger.is_none()
+ || self.custom_mode.read(cx).program.read(cx).is_empty(cx),
+ ),
+ ),
+ })
+ .child(
+ Button::new("debugger-spawn", "Start")
+ .on_click(cx.listener(|this, _, window, cx| match &this.mode {
+ NewSessionMode::Launch => {
+ this.launch_picker.update(cx, |picker, cx| {
+ picker.delegate.confirm(true, window, cx)
+ })
+ }
+ _ => this.start_new_session(window, cx),
+ }))
+ .disabled(match self.mode {
+ NewSessionMode::Launch => {
+ !self.launch_picker.read(cx).delegate.matches.is_empty()
+ }
+ NewSessionMode::Attach => {
+ self.debugger.is_none()
+ || self
+ .attach_mode
+ .read(cx)
+ .attach_picker
+ .read(cx)
+ .picker
+ .read(cx)
+ .delegate
+ .match_count()
+ == 0
+ }
+ NewSessionMode::Custom => {
+ self.debugger.is_none()
+ || self.custom_mode.read(cx).program.read(cx).is_empty(cx)
+ }
+ }),
),
)
}
@@ -670,38 +561,12 @@ impl Render for NewSessionModal {
impl EventEmitter<DismissEvent> for NewSessionModal {}
impl Focusable for NewSessionModal {
fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
- self.mode.focus_handle(cx)
+ self.mode_focus_handle(cx)
}
}
impl ModalView for NewSessionModal {}
-impl RenderOnce for LaunchMode {
- fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
- v_flex()
- .p_2()
- .w_full()
- .gap_3()
- .track_focus(&self.program.focus_handle(cx))
- .child(
- div().child(
- Label::new("Program")
- .size(ui::LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- .child(render_editor(&self.program, window, cx))
- .child(
- div().child(
- Label::new("Working Directory")
- .size(ui::LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- .child(render_editor(&self.cwd, window, cx))
- }
-}
-
impl RenderOnce for AttachMode {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
@@ -711,15 +576,14 @@ impl RenderOnce for AttachMode {
}
}
-use std::rc::Rc;
-
#[derive(Clone)]
-pub(super) struct LaunchMode {
+pub(super) struct CustomMode {
program: Entity<Editor>,
cwd: Entity<Editor>,
+ stop_on_entry: ToggleState,
}
-impl LaunchMode {
+impl CustomMode {
pub(super) fn new(
past_launch_config: Option<LaunchRequest>,
window: &mut Window,
@@ -744,10 +608,14 @@ impl LaunchMode {
this.set_text(past_cwd.to_string_lossy(), window, cx);
};
});
- cx.new(|_| Self { program, cwd })
+ cx.new(|_| Self {
+ program,
+ cwd,
+ stop_on_entry: ToggleState::Unselected,
+ })
}
- pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
+ pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
let path = self.cwd.read(cx).text(cx);
task::LaunchRequest {
program: self.program.read(cx).text(cx),
@@ -756,19 +624,66 @@ impl LaunchMode {
env: Default::default(),
}
}
+
+ fn render(
+ &mut self,
+ adapter_menu: DropdownMenu,
+ window: &mut Window,
+ cx: &mut ui::Context<Self>,
+ ) -> impl IntoElement {
+ v_flex()
+ .p_2()
+ .w_full()
+ .gap_3()
+ .track_focus(&self.program.focus_handle(cx))
+ .child(
+ div().child(
+ Label::new("Program")
+ .size(ui::LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(render_editor(&self.program, window, cx))
+ .child(
+ h_flex()
+ .child(
+ Label::new("Debugger")
+ .size(ui::LabelSize::Small)
+ .color(Color::Muted),
+ )
+ .gap(ui::DynamicSpacing::Base08.rems(cx))
+ .child(adapter_menu),
+ )
+ .child(
+ CheckboxWithLabel::new(
+ "debugger-stop-on-entry",
+ Label::new("Stop on Entry").size(ui::LabelSize::Small),
+ self.stop_on_entry,
+ {
+ let this = cx.weak_entity();
+ move |state, _, cx| {
+ this.update(cx, |this, _| {
+ this.stop_on_entry = *state;
+ })
+ .ok();
+ }
+ },
+ )
+ .checkbox_position(ui::IconPosition::End),
+ )
+ }
}
#[derive(Clone)]
pub(super) struct AttachMode {
pub(super) definition: DebugTaskDefinition,
pub(super) attach_picker: Entity<AttachModal>,
- _subscription: Rc<Subscription>,
}
impl AttachMode {
pub(super) fn new(
debugger: Option<DebugAdapterName>,
- workspace: Entity<Workspace>,
+ workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<NewSessionModal>,
) -> Entity<Self> {
@@ -787,17 +702,12 @@ impl AttachMode {
modal
});
- let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
- cx.emit(DismissEvent);
- });
-
cx.new(|_| Self {
definition,
attach_picker,
- _subscription: Rc::new(subscription),
})
}
- pub(super) fn debug_task(&self) -> task::AttachRequest {
+ pub(super) fn debug_request(&self) -> task::AttachRequest {
task::AttachRequest { process_id: None }
}
}
@@ -810,6 +720,7 @@ pub(super) struct DebugScenarioDelegate {
prompt: String,
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
+ task_contexts: Arc<TaskContexts>,
}
impl DebugScenarioDelegate {
@@ -817,6 +728,7 @@ impl DebugScenarioDelegate {
debug_panel: WeakEntity<DebugPanel>,
workspace: WeakEntity<Workspace>,
task_store: Entity<TaskStore>,
+ task_contexts: Arc<TaskContexts>,
) -> Self {
Self {
task_store,
@@ -826,6 +738,7 @@ impl DebugScenarioDelegate {
prompt: String::new(),
debug_panel,
workspace,
+ task_contexts,
}
}
}
@@ -860,45 +773,55 @@ impl PickerDelegate for DebugScenarioDelegate {
window: &mut Window,
cx: &mut Context<picker::Picker<Self>>,
) -> gpui::Task<()> {
- let candidates: Vec<_> = match &self.candidates {
- Some(candidates) => candidates
- .into_iter()
- .enumerate()
- .map(|(index, (_, candidate))| {
- StringMatchCandidate::new(index, candidate.label.as_ref())
- })
- .collect(),
- None => {
- let worktree_ids: Vec<_> = self
- .workspace
- .update(cx, |this, cx| {
- this.visible_worktrees(cx)
- .map(|tree| tree.read(cx).id())
- .collect()
- })
- .ok()
- .unwrap_or_default();
-
- let scenarios: Vec<_> = self
- .task_store
- .read(cx)
- .task_inventory()
- .map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
- .unwrap_or_default();
-
- self.candidates = Some(scenarios.clone());
+ let candidates = self.candidates.clone();
+ let workspace = self.workspace.clone();
+ let task_store = self.task_store.clone();
- scenarios
+ cx.spawn_in(window, async move |picker, cx| {
+ let candidates: Vec<_> = match &candidates {
+ Some(candidates) => candidates
.into_iter()
.enumerate()
.map(|(index, (_, candidate))| {
StringMatchCandidate::new(index, candidate.label.as_ref())
})
- .collect()
- }
- };
+ .collect(),
+ None => {
+ let worktree_ids: Vec<_> = workspace
+ .update(cx, |this, cx| {
+ this.visible_worktrees(cx)
+ .map(|tree| tree.read(cx).id())
+ .collect()
+ })
+ .ok()
+ .unwrap_or_default();
+
+ let scenarios: Vec<_> = task_store
+ .update(cx, |task_store, cx| {
+ task_store.task_inventory().map(|item| {
+ item.read(cx).list_debug_scenarios(worktree_ids.into_iter())
+ })
+ })
+ .ok()
+ .flatten()
+ .unwrap_or_default();
+
+ picker
+ .update(cx, |picker, _| {
+ picker.delegate.candidates = Some(scenarios.clone());
+ })
+ .ok();
+
+ scenarios
+ .into_iter()
+ .enumerate()
+ .map(|(index, (_, candidate))| {
+ StringMatchCandidate::new(index, candidate.label.as_ref())
+ })
+ .collect()
+ }
+ };
- cx.spawn_in(window, async move |picker, cx| {
let matches = fuzzy::match_strings(
&candidates,
&query,
@@ -864,7 +864,7 @@ impl RunningState {
dap::DebugRequest::Launch(new_launch_request)
}
- request @ dap::DebugRequest::Attach(_) => request,
+ request @ dap::DebugRequest::Attach(_) => request, // todo(debugger): We should check that process_id is valid and if not show the modal
};
Ok(DebugTaskDefinition {
label,
@@ -103,7 +103,7 @@ async fn test_show_attach_modal_and_select_process(
});
let attach_modal = workspace
.update(cx, |workspace, window, cx| {
- let workspace_handle = cx.entity();
+ let workspace_handle = cx.weak_entity();
workspace.toggle_modal(window, cx, |window, cx| {
AttachModal::with_processes(
workspace_handle,
@@ -141,7 +141,7 @@ impl JsonLspAdapter {
},
{
"fileMatch": [
- schema_file_match(paths::debug_tasks_file()),
+ schema_file_match(paths::debug_scenarios_file()),
paths::local_debug_file_relative_path()
],
"schema": debug_schema,
@@ -216,9 +216,9 @@ pub fn tasks_file() -> &'static PathBuf {
}
/// Returns the path to the `debug.json` file.
-pub fn debug_tasks_file() -> &'static PathBuf {
- static DEBUG_TASKS_FILE: OnceLock<PathBuf> = OnceLock::new();
- DEBUG_TASKS_FILE.get_or_init(|| config_dir().join("debug.json"))
+pub fn debug_scenarios_file() -> &'static PathBuf {
+ static DEBUG_SCENARIOS_FILE: OnceLock<PathBuf> = OnceLock::new();
+ DEBUG_SCENARIOS_FILE.get_or_init(|| config_dir().join("debug.json"))
}
/// Returns the path to the extensions directory.
@@ -193,22 +193,22 @@ pub struct DebugScenario {
/// Name of the debug task
pub label: SharedString,
/// A task to run prior to spawning the debuggee.
- #[serde(default)]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub build: Option<BuildTaskDefinition>,
#[serde(flatten)]
pub request: Option<DebugRequest>,
/// Additional initialization arguments to be sent on DAP initialization
- #[serde(default)]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub initialize_args: Option<serde_json::Value>,
/// Optional TCP connection information
///
/// If provided, this will be used to connect to the debug adapter instead of
/// spawning a new process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process.
- #[serde(default)]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub tcp_connection: Option<TcpArgumentsTemplate>,
/// Whether to tell the debug adapter to stop on entry
- #[serde(default)]
+ #[serde(default, skip_serializing_if = "Option::is_none")]
pub stop_on_entry: Option<bool>,
}
@@ -701,7 +701,7 @@ fn register_actions(
})
.register_action(move |_: &mut Workspace, _: &OpenDebugTasks, window, cx| {
open_settings_file(
- paths::debug_tasks_file(),
+ paths::debug_scenarios_file(),
|| settings::initial_debug_tasks_content().as_ref().into(),
window,
cx,