{
+ let active_session = self
+ .pane
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::
());
+ Some(
+ h_flex()
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ .p_1()
+ .justify_between()
+ .w_full()
+ .child(
+ h_flex().gap_2().w_full().when_some(
+ active_session
+ .as_ref()
+ .and_then(|session| session.read(cx).mode().as_running()),
+ |this, running_session| {
+ let thread_status = running_session
+ .read(cx)
+ .thread_status(cx)
+ .unwrap_or(project::debugger::session::ThreadStatus::Exited);
+ let capabilities = running_session.read(cx).capabilities(cx);
+ this.map(|this| {
+ if thread_status == ThreadStatus::Running {
+ this.child(
+ IconButton::new("debug-pause", IconName::DebugPause)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .on_click(window.listener_for(
+ &running_session,
+ |this, _, _window, cx| {
+ this.pause_thread(cx);
+ },
+ ))
+ .tooltip(move |window, cx| {
+ Tooltip::text("Pause program")(window, cx)
+ }),
+ )
+ } else {
+ this.child(
+ IconButton::new("debug-continue", IconName::DebugContinue)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .on_click(window.listener_for(
+ &running_session,
+ |this, _, _window, cx| this.continue_thread(cx),
+ ))
+ .disabled(thread_status != ThreadStatus::Stopped)
+ .tooltip(move |window, cx| {
+ Tooltip::text("Continue program")(window, cx)
+ }),
+ )
+ }
+ })
+ .child(
+ IconButton::new("debug-step-over", IconName::ArrowRight)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .on_click(window.listener_for(
+ &running_session,
+ |this, _, _window, cx| {
+ this.step_over(cx);
+ },
+ ))
+ .disabled(thread_status != ThreadStatus::Stopped)
+ .tooltip(move |window, cx| {
+ Tooltip::text("Step over")(window, cx)
+ }),
+ )
+ .child(
+ IconButton::new("debug-step-out", IconName::ArrowUpRight)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .on_click(window.listener_for(
+ &running_session,
+ |this, _, _window, cx| {
+ this.step_out(cx);
+ },
+ ))
+ .disabled(thread_status != ThreadStatus::Stopped)
+ .tooltip(move |window, cx| {
+ Tooltip::text("Step out")(window, cx)
+ }),
+ )
+ .child(
+ IconButton::new("debug-step-into", IconName::ArrowDownRight)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .on_click(window.listener_for(
+ &running_session,
+ |this, _, _window, cx| {
+ this.step_in(cx);
+ },
+ ))
+ .disabled(thread_status != ThreadStatus::Stopped)
+ .tooltip(move |window, cx| {
+ Tooltip::text("Step in")(window, cx)
+ }),
+ )
+ .child(Divider::vertical())
+ .child(
+ IconButton::new(
+ "debug-enable-breakpoint",
+ IconName::DebugDisabledBreakpoint,
+ )
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .disabled(thread_status != ThreadStatus::Stopped),
+ )
+ .child(
+ IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .disabled(thread_status != ThreadStatus::Stopped),
+ )
+ .child(
+ IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
+ .icon_size(IconSize::XSmall)
+ .shape(ui::IconButtonShape::Square)
+ .disabled(
+ thread_status == ThreadStatus::Exited
+ || thread_status == ThreadStatus::Ended,
+ )
+ .on_click(window.listener_for(
+ &running_session,
+ |this, _, _window, cx| {
+ this.toggle_ignore_breakpoints(cx);
+ },
+ ))
+ .tooltip(move |window, cx| {
+ Tooltip::text("Disable all breakpoints")(window, cx)
+ }),
+ )
+ .child(Divider::vertical())
+ .child(
+ IconButton::new("debug-restart", IconName::DebugRestart)
+ .icon_size(IconSize::XSmall)
+ .on_click(window.listener_for(
+ &running_session,
+ |this, _, _window, cx| {
+ this.restart_session(cx);
+ },
+ ))
+ .disabled(
+ !capabilities.supports_restart_request.unwrap_or_default(),
+ )
+ .tooltip(move |window, cx| {
+ Tooltip::text("Restart")(window, cx)
+ }),
+ )
+ .child(
+ IconButton::new("debug-stop", IconName::Power)
+ .icon_size(IconSize::XSmall)
+ .on_click(window.listener_for(
+ &running_session,
+ |this, _, _window, cx| {
+ this.stop_thread(cx);
+ },
+ ))
+ .disabled(
+ thread_status != ThreadStatus::Stopped
+ && thread_status != ThreadStatus::Running,
+ )
+ .tooltip({
+ let label = if capabilities
+ .supports_terminate_threads_request
+ .unwrap_or_default()
+ {
+ "Terminate Thread"
+ } else {
+ "Terminate all Threads"
+ };
+ move |window, cx| Tooltip::text(label)(window, cx)
+ }),
+ )
+ },
+ ),
+ )
+ .child(
+ h_flex()
+ .gap_2()
+ .when_some(
+ active_session
+ .as_ref()
+ .and_then(|session| session.read(cx).mode().as_running())
+ .cloned(),
+ |this, session| {
+ this.child(
+ session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
+ )
+ .child(Divider::vertical())
+ },
+ )
+ .when_some(active_session.as_ref(), |this, session| {
+ let pane = self.pane.downgrade();
+ let label = session.read(cx).label(cx);
+ this.child(DropdownMenu::new(
+ "debugger-session-list",
+ label,
+ ContextMenu::build(window, cx, move |mut this, _, cx| {
+ let sessions = pane
+ .read_with(cx, |pane, _| {
+ pane.items().map(|item| item.boxed_clone()).collect()
+ })
+ .ok()
+ .unwrap_or_else(Vec::new);
+ for (index, item) in sessions.into_iter().enumerate() {
+ if let Some(session) = item.downcast::() {
+ let pane = pane.clone();
+ this = this.entry(
+ session.read(cx).label(cx),
+ None,
+ move |window, cx| {
+ pane.update(cx, |pane, cx| {
+ pane.activate_item(
+ index, true, true, window, cx,
+ );
+ })
+ .ok();
+ },
+ );
+ }
+ }
+ this
+ }),
+ ))
+ .child(Divider::vertical())
+ })
+ .child(
+ IconButton::new("debug-new-session", IconName::Plus)
+ .icon_size(IconSize::Small)
+ .on_click({
+ let workspace = self.workspace.clone();
+ let weak_panel = cx.weak_entity();
+ let past_debug_definition = self.past_debug_definition.clone();
+ move |_, window, cx| {
+ let weak_panel = weak_panel.clone();
+ let past_debug_definition = past_debug_definition.clone();
+
+ let _ = workspace.update(cx, |this, cx| {
+ let workspace = cx.weak_entity();
+ this.toggle_modal(window, cx, |window, cx| {
+ NewSessionModal::new(
+ past_debug_definition,
+ weak_panel,
+ workspace,
+ window,
+ cx,
+ )
+ });
+ });
+ }
+ })
+ .tooltip(|window, cx| {
+ Tooltip::for_action(
+ "New Debug Session",
+ &CreateDebuggingSession,
+ window,
+ cx,
+ )
+ }),
+ ),
+ ),
+ )
+ }
}
impl EventEmitter for DebugPanel {}
@@ -507,7 +717,7 @@ impl Panel for DebugPanel {
) {
}
- fn size(&self, _window: &Window, _cx: &App) -> Pixels {
+ fn size(&self, _window: &Window, _: &App) -> Pixels {
self.size
}
@@ -538,42 +748,49 @@ impl Panel for DebugPanel {
fn activation_priority(&self) -> u32 {
9
}
- fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) {
- if active && self.pane.read(cx).items_len() == 0 {
- let Some(project) = self.project.clone().upgrade() else {
- return;
- };
- let config = self.last_inert_config.clone();
- let panel = cx.weak_entity();
- // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
- self.pane.update(cx, |this, cx| {
- this.add_item(
- Box::new(DebugSession::inert(
- project,
- self.workspace.clone(),
- panel,
- config,
- window,
- cx,
- )),
- false,
- false,
- None,
- window,
- cx,
- );
- });
- }
- }
+ fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context) {}
}
impl Render for DebugPanel {
- fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ let has_sessions = self.pane.read(cx).items_len() > 0;
v_flex()
- .key_context("DebugPanel")
- .track_focus(&self.focus_handle(cx))
.size_full()
- .child(self.pane.clone())
+ .key_context("DebugPanel")
+ .child(h_flex().children(self.top_controls_strip(window, cx)))
+ .track_focus(&self.focus_handle(cx))
+ .map(|this| {
+ if has_sessions {
+ this.child(self.pane.clone())
+ } else {
+ this.child(
+ v_flex()
+ .h_full()
+ .gap_1()
+ .items_center()
+ .justify_center()
+ .child(
+ h_flex().child(
+ Label::new("No Debugging Sessions")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ ),
+ )
+ .child(
+ h_flex().flex_shrink().child(
+ Button::new("spawn-new-session-empty-state", "New Session")
+ .size(ButtonSize::Large)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ CreateDebuggingSession.boxed_clone(),
+ cx,
+ );
+ }),
+ ),
+ ),
+ )
+ }
+ })
.into_any()
}
}
diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs
index ea1b2cd5e2..af84e4bab5 100644
--- a/crates/debugger_ui/src/debugger_ui.rs
+++ b/crates/debugger_ui/src/debugger_ui.rs
@@ -1,20 +1,38 @@
use dap::debugger_settings::DebuggerSettings;
use debugger_panel::{DebugPanel, ToggleFocus};
use feature_flags::{Debugger, FeatureFlagViewExt};
-use gpui::App;
+use gpui::{App, actions};
+use new_session_modal::NewSessionModal;
use session::DebugSession;
use settings::Settings;
-use workspace::{
- Pause, Restart, ShutdownDebugAdapters, StepBack, StepInto, StepOver, Stop,
- ToggleIgnoreBreakpoints, Workspace,
-};
+use workspace::{ShutdownDebugAdapters, Workspace};
pub mod attach_modal;
pub mod debugger_panel;
-pub mod session;
+mod new_session_modal;
+pub(crate) mod session;
#[cfg(test)]
-mod tests;
+pub mod tests;
+
+actions!(
+ debugger,
+ [
+ Start,
+ Continue,
+ Disconnect,
+ Pause,
+ Restart,
+ StepInto,
+ StepOver,
+ StepOut,
+ StepBack,
+ Stop,
+ ToggleIgnoreBreakpoints,
+ ClearAllBreakpoints,
+ CreateDebuggingSession,
+ ]
+);
pub fn init(cx: &mut App) {
DebuggerSettings::register(cx);
@@ -115,6 +133,23 @@ pub fn init(cx: &mut App) {
})
})
},
+ )
+ .register_action(
+ |workspace: &mut Workspace, _: &CreateDebuggingSession, window, cx| {
+ let debug_panel = workspace.panel::(cx).unwrap();
+ let weak_panel = debug_panel.downgrade();
+ let weak_workspace = cx.weak_entity();
+
+ workspace.toggle_modal(window, cx, |window, cx| {
+ NewSessionModal::new(
+ debug_panel.read(cx).past_debug_definition.clone(),
+ weak_panel,
+ weak_workspace,
+ window,
+ cx,
+ )
+ });
+ },
);
})
})
diff --git a/crates/debugger_ui/src/new_session_modal.rs b/crates/debugger_ui/src/new_session_modal.rs
new file mode 100644
index 0000000000..8664a75291
--- /dev/null
+++ b/crates/debugger_ui/src/new_session_modal.rs
@@ -0,0 +1,633 @@
+use std::{
+ borrow::Cow,
+ ops::Not,
+ path::{Path, PathBuf},
+};
+
+use anyhow::{Result, anyhow};
+use dap::DebugRequestType;
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{
+ App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
+ WeakEntity,
+};
+use settings::Settings;
+use task::{DebugTaskDefinition, LaunchConfig};
+use theme::ThemeSettings;
+use ui::{
+ ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
+ ContextMenu, Disableable, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
+ LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
+ ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
+};
+use util::ResultExt;
+use workspace::{ModalView, Workspace};
+
+use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
+
+#[derive(Clone)]
+pub(super) struct NewSessionModal {
+ workspace: WeakEntity,
+ debug_panel: WeakEntity,
+ mode: NewSessionMode,
+ stop_on_entry: ToggleState,
+ debugger: Option,
+ last_selected_profile_name: Option,
+}
+
+fn suggested_label(request: &DebugRequestType, debugger: &str) -> String {
+ match request {
+ DebugRequestType::Launch(config) => {
+ let last_path_component = Path::new(&config.program)
+ .file_name()
+ .map(|name| name.to_string_lossy())
+ .unwrap_or_else(|| Cow::Borrowed(&config.program));
+
+ format!("{} ({debugger})", last_path_component)
+ }
+ DebugRequestType::Attach(config) => format!(
+ "pid: {} ({debugger})",
+ config.process_id.unwrap_or(u32::MAX)
+ ),
+ }
+}
+
+impl NewSessionModal {
+ pub(super) fn new(
+ past_debug_definition: Option,
+ debug_panel: WeakEntity,
+ workspace: WeakEntity,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self {
+ let debugger = past_debug_definition
+ .as_ref()
+ .map(|def| def.adapter.clone().into());
+
+ let stop_on_entry = past_debug_definition
+ .as_ref()
+ .and_then(|def| def.stop_on_entry);
+
+ let launch_config = match past_debug_definition.map(|def| def.request) {
+ Some(DebugRequestType::Launch(launch_config)) => Some(launch_config),
+ _ => None,
+ };
+
+ 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,
+ }
+ }
+
+ fn debug_config(&self, cx: &App) -> Option {
+ let request = self.mode.debug_task(cx);
+
+ Some(DebugTaskDefinition {
+ adapter: self.debugger.clone()?.to_string(),
+ label: suggested_label(&request, self.debugger.as_deref()?),
+ request,
+ initialize_args: None,
+ tcp_connection: None,
+ locator: None,
+ stop_on_entry: match self.stop_on_entry {
+ ToggleState::Selected => Some(true),
+ _ => None,
+ },
+ })
+ }
+ fn start_new_session(&self, cx: &mut Context) -> Result<()> {
+ let workspace = self.workspace.clone();
+ let config = self
+ .debug_config(cx)
+ .ok_or_else(|| anyhow!("Failed to create a debug config"))?;
+
+ let _ = self.debug_panel.update(cx, |panel, _| {
+ panel.past_debug_definition = Some(config.clone());
+ });
+
+ cx.spawn(async move |this, cx| {
+ let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
+ let task =
+ project.update(cx, |this, cx| this.start_debug_session(config.into(), cx))?;
+ let spawn_result = task.await;
+ if spawn_result.is_ok() {
+ this.update(cx, |_, cx| {
+ cx.emit(DismissEvent);
+ })
+ .ok();
+ }
+ spawn_result?;
+ anyhow::Result::<_, anyhow::Error>::Ok(())
+ })
+ .detach_and_log_err(cx);
+ Ok(())
+ }
+
+ fn update_attach_picker(
+ attach: &Entity,
+ selected_debugger: &str,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ attach.update(cx, |this, cx| {
+ if selected_debugger != this.debug_definition.adapter {
+ this.debug_definition.adapter = selected_debugger.into();
+ if let Some(project) = this
+ .workspace
+ .read_with(cx, |workspace, _| workspace.project().clone())
+ .ok()
+ {
+ this.attach_picker = Some(cx.new(|cx| {
+ let modal = AttachModal::new(
+ project,
+ this.debug_definition.clone(),
+ false,
+ window,
+ cx,
+ );
+
+ window.focus(&modal.focus_handle(cx));
+
+ modal
+ }));
+ }
+ }
+
+ cx.notify();
+ })
+ }
+ fn adapter_drop_down_menu(
+ &self,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> ui::DropdownMenu {
+ let workspace = self.workspace.clone();
+ let weak = cx.weak_entity();
+ let debugger = self.debugger.clone();
+ DropdownMenu::new(
+ "dap-adapter-picker",
+ debugger
+ .as_ref()
+ .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
+ .clone(),
+ ContextMenu::build(window, cx, move |mut menu, _, cx| {
+ let setter_for_name = |name: SharedString| {
+ let weak = weak.clone();
+ move |window: &mut Window, cx: &mut App| {
+ 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);
+ }
+ })
+ .ok();
+ }
+ };
+
+ let available_adapters = workspace
+ .update(cx, |this, cx| {
+ this.project()
+ .read(cx)
+ .debug_adapters()
+ .enumerate_adapters()
+ })
+ .ok()
+ .unwrap_or_default();
+
+ for adapter in available_adapters {
+ menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.0.clone()));
+ }
+ menu
+ }),
+ )
+ }
+
+ fn debug_config_drop_down_menu(
+ &self,
+ window: &mut Window,
+ cx: &mut Context,
+ ) -> ui::DropdownMenu {
+ let workspace = self.workspace.clone();
+ let weak = cx.weak_entity();
+ let last_profile = self.last_selected_profile_name.clone();
+ 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: DebugTaskDefinition| {
+ let weak = weak.clone();
+ let workspace = workspace.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(task.adapter.clone().into());
+
+ match &task.request {
+ DebugRequestType::Launch(launch_config) => {
+ this.mode = NewSessionMode::launch(
+ Some(launch_config.clone()),
+ window,
+ cx,
+ );
+ }
+ DebugRequestType::Attach(_) => {
+ this.mode = NewSessionMode::attach(
+ this.debugger.clone(),
+ workspace.clone(),
+ window,
+ cx,
+ );
+ if let Some((debugger, attach)) =
+ this.debugger.as_ref().zip(this.mode.as_attach())
+ {
+ Self::update_attach_picker(&attach, &debugger, window, cx);
+ }
+ }
+ }
+ cx.notify();
+ })
+ .ok();
+ }
+ };
+
+ let available_adapters: Vec = 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_tasks())
+ .cloned()
+ .filter_map(|task| task.try_into().ok())
+ .collect()
+ })
+ .ok()
+ .unwrap_or_default();
+
+ for debug_definition in available_adapters {
+ menu = menu.entry(
+ debug_definition.label.clone(),
+ None,
+ setter_for_name(debug_definition),
+ );
+ }
+ menu
+ }),
+ )
+ }
+}
+
+#[derive(Clone)]
+struct LaunchMode {
+ program: Entity,
+ cwd: Entity,
+}
+
+impl LaunchMode {
+ fn new(
+ past_launch_config: Option,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Entity {
+ let (past_program, past_cwd) = past_launch_config
+ .map(|config| (Some(config.program), config.cwd))
+ .unwrap_or_else(|| (None, None));
+
+ let program = cx.new(|cx| Editor::single_line(window, cx));
+ program.update(cx, |this, cx| {
+ this.set_placeholder_text("Program path", cx);
+
+ if let Some(past_program) = past_program {
+ this.set_text(past_program, window, cx);
+ };
+ });
+ let cwd = cx.new(|cx| Editor::single_line(window, cx));
+ cwd.update(cx, |this, cx| {
+ this.set_placeholder_text("Working Directory", cx);
+ if let Some(past_cwd) = past_cwd {
+ this.set_text(past_cwd.to_string_lossy(), window, cx);
+ };
+ });
+ cx.new(|_| Self { program, cwd })
+ }
+
+ fn debug_task(&self, cx: &App) -> task::LaunchConfig {
+ let path = self.cwd.read(cx).text(cx);
+ task::LaunchConfig {
+ program: self.program.read(cx).text(cx),
+ cwd: path.is_empty().not().then(|| PathBuf::from(path)),
+ args: Default::default(),
+ }
+ }
+}
+
+#[derive(Clone)]
+struct AttachMode {
+ workspace: WeakEntity,
+ debug_definition: DebugTaskDefinition,
+ attach_picker: Option>,
+ focus_handle: FocusHandle,
+}
+
+impl AttachMode {
+ fn new(
+ debugger: Option,
+ workspace: WeakEntity,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Entity {
+ let debug_definition = DebugTaskDefinition {
+ label: "Attach New Session Setup".into(),
+ request: dap::DebugRequestType::Attach(task::AttachConfig { process_id: None }),
+ tcp_connection: None,
+ adapter: debugger.clone().unwrap_or_default().into(),
+ locator: None,
+ initialize_args: None,
+ stop_on_entry: Some(false),
+ };
+
+ let attach_picker = if let Some(project) = debugger.and(
+ workspace
+ .read_with(cx, |workspace, _| workspace.project().clone())
+ .ok(),
+ ) {
+ Some(cx.new(|cx| {
+ let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
+ window.focus(&modal.focus_handle(cx));
+
+ modal
+ }))
+ } else {
+ None
+ };
+
+ cx.new(|cx| Self {
+ workspace,
+ debug_definition,
+ attach_picker,
+ focus_handle: cx.focus_handle(),
+ })
+ }
+ fn debug_task(&self) -> task::AttachConfig {
+ task::AttachConfig { process_id: None }
+ }
+}
+
+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),
+ Attach(Entity),
+}
+
+impl NewSessionMode {
+ fn debug_task(&self, cx: &App) -> DebugRequestType {
+ match self {
+ NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
+ NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
+ }
+ }
+ fn as_attach(&self) -> Option<&Entity> {
+ if let NewSessionMode::Attach(entity) = self {
+ Some(entity)
+ } else {
+ None
+ }
+ }
+}
+
+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).focus_handle.clone(),
+ }
+ }
+}
+
+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, _: &mut App) -> impl IntoElement {
+ v_flex().w_full().children(self.attach_picker.clone())
+ }
+}
+
+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()
+ }),
+ }
+ }
+}
+
+impl NewSessionMode {
+ fn attach(
+ debugger: Option,
+ workspace: WeakEntity,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Self {
+ Self::Attach(AttachMode::new(debugger, workspace, window, cx))
+ }
+ fn launch(past_launch_config: Option, window: &mut Window, cx: &mut App) -> Self {
+ Self::Launch(LaunchMode::new(past_launch_config, window, cx))
+ }
+}
+fn render_editor(editor: &Entity, window: &mut Window, cx: &App) -> impl IntoElement {
+ let settings = ThemeSettings::get_global(cx);
+ let theme = cx.theme();
+
+ let text_style = TextStyle {
+ color: cx.theme().colors().text,
+ font_family: settings.buffer_font.family.clone(),
+ font_features: settings.buffer_font.features.clone(),
+ font_size: settings.buffer_font_size(cx).into(),
+ font_weight: settings.buffer_font.weight,
+ line_height: relative(settings.buffer_line_height.value()),
+ background_color: Some(theme.colors().editor_background),
+ ..Default::default()
+ };
+
+ let element = EditorElement::new(
+ editor,
+ EditorStyle {
+ background: theme.colors().editor_background,
+ local_player: theme.players().local(),
+ text: text_style,
+ ..Default::default()
+ },
+ );
+
+ div()
+ .rounded_md()
+ .p_1()
+ .border_1()
+ .border_color(theme.colors().border_variant)
+ .when(
+ editor.focus_handle(cx).contains_focused(window, cx),
+ |this| this.border_color(theme.colors().border_focused),
+ )
+ .child(element)
+ .bg(theme.colors().editor_background)
+}
+
+impl Render for NewSessionModal {
+ fn render(
+ &mut self,
+ window: &mut ui::Window,
+ cx: &mut ui::Context,
+ ) -> impl ui::IntoElement {
+ v_flex()
+ .size_full()
+ .w(rems(34.))
+ .elevation_3(cx)
+ .bg(cx.theme().colors().elevated_surface_background)
+ .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
+ cx.emit(DismissEvent);
+ }))
+ .child(
+ h_flex()
+ .w_full()
+ .justify_around()
+ .p_2()
+ .child(
+ h_flex()
+ .justify_start()
+ .w_full()
+ .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();
+ }))
+ .first(),
+ )
+ .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| {
+ this.mode = NewSessionMode::attach(
+ this.debugger.clone(),
+ this.workspace.clone(),
+ window,
+ cx,
+ );
+ if let Some((debugger, attach)) =
+ this.debugger.as_ref().zip(this.mode.as_attach())
+ {
+ Self::update_attach_picker(&attach, &debugger, window, cx);
+ }
+ this.mode.focus_handle(cx).focus(window);
+ cx.notify();
+ }))
+ .last(),
+ ),
+ )
+ .justify_between()
+ .child(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(
+ h_flex()
+ .justify_between()
+ .gap_2()
+ .p_2()
+ .border_color(cx.theme().colors().border_variant)
+ .border_t_1()
+ .w_full()
+ .child(self.debug_config_drop_down_menu(window, cx))
+ .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, _, _, cx| {
+ this.start_new_session(cx).log_err();
+ }))
+ .disabled(self.debugger.is_none()),
+ ),
+ ),
+ )
+ }
+}
+
+impl EventEmitter for NewSessionModal {}
+impl Focusable for NewSessionModal {
+ fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
+ self.mode.focus_handle(cx)
+ }
+}
+
+impl ModalView for NewSessionModal {}
diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs
index fd9e76c9fe..a08fb8f584 100644
--- a/crates/debugger_ui/src/session.rs
+++ b/crates/debugger_ui/src/session.rs
@@ -1,26 +1,13 @@
-mod failed;
-mod inert;
pub mod running;
-mod starting;
-
-use std::time::Duration;
use dap::client::SessionId;
-use failed::FailedState;
-use gpui::{
- Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable,
- Subscription, Task, Transformation, WeakEntity, percentage,
-};
-use inert::{InertEvent, InertState};
+use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
use project::Project;
use project::debugger::{dap_store::DapStore, session::Session};
use project::worktree_store::WorktreeStore;
use rpc::proto::{self, PeerId};
use running::RunningState;
-use starting::{StartingEvent, StartingState};
-use task::DebugTaskDefinition;
-use ui::{Indicator, prelude::*};
-use util::ResultExt;
+use ui::prelude::*;
use workspace::{
FollowableItem, ViewId, Workspace,
item::{self, Item},
@@ -29,9 +16,6 @@ use workspace::{
use crate::debugger_panel::DebugPanel;
pub(crate) enum DebugSessionState {
- Inert(Entity),
- Starting(Entity),
- Failed(Entity),
Running(Entity),
}
@@ -39,7 +23,6 @@ impl DebugSessionState {
pub(crate) fn as_running(&self) -> Option<&Entity> {
match &self {
DebugSessionState::Running(entity) => Some(entity),
- _ => None,
}
}
}
@@ -48,9 +31,9 @@ pub struct DebugSession {
remote_id: Option,
mode: DebugSessionState,
dap_store: WeakEntity,
- debug_panel: WeakEntity,
- worktree_store: WeakEntity,
- workspace: WeakEntity,
+ _debug_panel: WeakEntity,
+ _worktree_store: WeakEntity,
+ _workspace: WeakEntity,
_subscriptions: [Subscription; 1],
}
@@ -69,46 +52,11 @@ pub enum ThreadItem {
}
impl DebugSession {
- pub(super) fn inert(
- project: Entity,
- workspace: WeakEntity,
- debug_panel: WeakEntity,
- config: Option,
- window: &mut Window,
- cx: &mut App,
- ) -> Entity {
- let default_cwd = project
- .read(cx)
- .worktrees(cx)
- .next()
- .and_then(|tree| tree.read(cx).abs_path().to_str().map(|str| str.to_string()))
- .unwrap_or_default();
-
- let inert =
- cx.new(|cx| InertState::new(workspace.clone(), &default_cwd, config, window, cx));
-
- let project = project.read(cx);
- let dap_store = project.dap_store().downgrade();
- let worktree_store = project.worktree_store().downgrade();
- cx.new(|cx| {
- let _subscriptions = [cx.subscribe_in(&inert, window, Self::on_inert_event)];
- Self {
- remote_id: None,
- mode: DebugSessionState::Inert(inert),
- dap_store,
- worktree_store,
- debug_panel,
- workspace,
- _subscriptions,
- }
- })
- }
-
pub(crate) fn running(
project: Entity,
workspace: WeakEntity,
session: Entity,
- debug_panel: WeakEntity,
+ _debug_panel: WeakEntity,
window: &mut Window,
cx: &mut App,
) -> Entity {
@@ -121,26 +69,20 @@ impl DebugSession {
remote_id: None,
mode: DebugSessionState::Running(mode),
dap_store: project.read(cx).dap_store().downgrade(),
- debug_panel,
- worktree_store: project.read(cx).worktree_store().downgrade(),
- workspace,
+ _debug_panel,
+ _worktree_store: project.read(cx).worktree_store().downgrade(),
+ _workspace: workspace,
})
}
pub(crate) fn session_id(&self, cx: &App) -> Option {
match &self.mode {
- DebugSessionState::Inert(_) => None,
- DebugSessionState::Starting(entity) => Some(entity.read(cx).session_id),
- DebugSessionState::Failed(_) => None,
DebugSessionState::Running(entity) => Some(entity.read(cx).session_id()),
}
}
pub(crate) fn shutdown(&mut self, cx: &mut Context) {
match &self.mode {
- DebugSessionState::Inert(_) => {}
- DebugSessionState::Starting(_entity) => {} // todo(debugger): we need to shutdown the starting process in this case (or recreate it on a breakpoint being hit)
- DebugSessionState::Failed(_) => {}
DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
}
}
@@ -149,63 +91,29 @@ impl DebugSession {
&self.mode
}
- fn on_inert_event(
- &mut self,
- _: &Entity,
- event: &InertEvent,
- window: &mut Window,
- cx: &mut Context,
- ) {
- let dap_store = self.dap_store.clone();
- let InertEvent::Spawned { config } = event;
- let config = config.clone();
-
- self.debug_panel
- .update(cx, |this, _| this.last_inert_config = Some(config.clone()))
- .log_err();
-
- let worktree = self
- .worktree_store
- .update(cx, |this, _| this.worktrees().next())
- .ok()
- .flatten()
- .expect("worktree-less project");
- let Ok((new_session_id, task)) = dap_store.update(cx, |store, cx| {
- store.new_session(config.into(), &worktree, None, cx)
- }) else {
- return;
+ pub(crate) fn label(&self, cx: &App) -> String {
+ let session_id = match &self.mode {
+ DebugSessionState::Running(running_state) => running_state.read(cx).session_id(),
};
- let starting = cx.new(|cx| StartingState::new(new_session_id, task, cx));
-
- self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)];
- self.mode = DebugSessionState::Starting(starting);
- }
-
- fn on_starting_event(
- &mut self,
- _: &Entity,
- event: &StartingEvent,
- window: &mut Window,
- cx: &mut Context,
- ) {
- if let StartingEvent::Finished(session) = event {
- let mode =
- cx.new(|cx| RunningState::new(session.clone(), self.workspace.clone(), window, cx));
- self.mode = DebugSessionState::Running(mode);
- } else if let StartingEvent::Failed = event {
- self.mode = DebugSessionState::Failed(cx.new(FailedState::new));
+ let Ok(Some(session)) = self
+ .dap_store
+ .read_with(cx, |store, _| store.session_by_id(session_id))
+ else {
+ return "".to_owned();
};
- cx.notify();
+ session
+ .read(cx)
+ .as_local()
+ .expect("Remote Debug Sessions are not implemented yet")
+ .label()
}
}
+
impl EventEmitter for DebugSession {}
impl Focusable for DebugSession {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
- DebugSessionState::Inert(inert_state) => inert_state.focus_handle(cx),
- DebugSessionState::Starting(starting_state) => starting_state.focus_handle(cx),
- DebugSessionState::Failed(failed_state) => failed_state.focus_handle(cx),
DebugSessionState::Running(running_state) => running_state.focus_handle(cx),
}
}
@@ -213,61 +121,6 @@ impl Focusable for DebugSession {
impl Item for DebugSession {
type Event = DebugPanelItemEvent;
- fn tab_content(&self, _: item::TabContentParams, _: &Window, cx: &App) -> AnyElement {
- let (icon, label, color) = match &self.mode {
- DebugSessionState::Inert(_) => (None, "New Session", Color::Default),
- DebugSessionState::Starting(_) => (None, "Starting", Color::Default),
- DebugSessionState::Failed(_) => (
- Some(Indicator::dot().color(Color::Error)),
- "Failed",
- Color::Error,
- ),
- DebugSessionState::Running(state) => {
- if state.read(cx).session().read(cx).is_terminated() {
- (
- Some(Indicator::dot().color(Color::Error)),
- "Terminated",
- Color::Error,
- )
- } else {
- match state.read(cx).thread_status(cx).unwrap_or_default() {
- project::debugger::session::ThreadStatus::Stopped => (
- Some(Indicator::dot().color(Color::Conflict)),
- state
- .read_with(cx, |state, cx| state.thread_status(cx))
- .map(|status| status.label())
- .unwrap_or("Stopped"),
- Color::Conflict,
- ),
- _ => (
- Some(Indicator::dot().color(Color::Success)),
- state
- .read_with(cx, |state, cx| state.thread_status(cx))
- .map(|status| status.label())
- .unwrap_or("Running"),
- Color::Success,
- ),
- }
- }
- }
- };
-
- let is_starting = matches!(self.mode, DebugSessionState::Starting(_));
-
- h_flex()
- .gap_2()
- .children(is_starting.then(|| {
- Icon::new(IconName::ArrowCircle).with_animation(
- "starting-debug-session",
- Animation::new(Duration::from_secs(2)).repeat(),
- |this, delta| this.transform(Transformation::rotate(percentage(delta))),
- )
- }))
- .when_some(icon, |this, indicator| this.child(indicator))
- .justify_between()
- .child(Label::new(label).color(color))
- .into_any_element()
- }
}
impl FollowableItem for DebugSession {
@@ -339,15 +192,6 @@ impl FollowableItem for DebugSession {
impl Render for DebugSession {
fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
match &self.mode {
- DebugSessionState::Inert(inert_state) => {
- inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
- }
- DebugSessionState::Starting(starting_state) => {
- starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
- }
- DebugSessionState::Failed(failed_state) => {
- failed_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
- }
DebugSessionState::Running(running_state) => {
running_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
}
diff --git a/crates/debugger_ui/src/session/failed.rs b/crates/debugger_ui/src/session/failed.rs
deleted file mode 100644
index c94f228491..0000000000
--- a/crates/debugger_ui/src/session/failed.rs
+++ /dev/null
@@ -1,30 +0,0 @@
-use gpui::{FocusHandle, Focusable};
-use ui::{
- Color, Context, IntoElement, Label, LabelCommon, ParentElement, Render, Styled, Window, h_flex,
-};
-
-pub(crate) struct FailedState {
- focus_handle: FocusHandle,
-}
-impl FailedState {
- pub(super) fn new(cx: &mut Context) -> Self {
- Self {
- focus_handle: cx.focus_handle(),
- }
- }
-}
-
-impl Focusable for FailedState {
- fn focus_handle(&self, _: &ui::App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-impl Render for FailedState {
- fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement {
- h_flex()
- .size_full()
- .items_center()
- .justify_center()
- .child(Label::new("Failed to spawn debugging session").color(Color::Error))
- }
-}
diff --git a/crates/debugger_ui/src/session/inert.rs b/crates/debugger_ui/src/session/inert.rs
deleted file mode 100644
index 03d308fe4f..0000000000
--- a/crates/debugger_ui/src/session/inert.rs
+++ /dev/null
@@ -1,337 +0,0 @@
-use std::path::PathBuf;
-
-use dap::DebugRequestType;
-use editor::{Editor, EditorElement, EditorStyle};
-use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
-use settings::Settings as _;
-use task::{DebugTaskDefinition, LaunchConfig, TCPHost};
-use theme::ThemeSettings;
-use ui::{
- ActiveTheme as _, ButtonCommon, ButtonLike, Clickable, Context, ContextMenu, Disableable,
- DropdownMenu, FluentBuilder, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
- LabelCommon, LabelSize, ParentElement, PopoverMenu, PopoverMenuHandle, Render, SharedString,
- SplitButton, Styled, Window, div, h_flex, relative, v_flex,
-};
-use workspace::Workspace;
-
-use crate::attach_modal::AttachModal;
-
-#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
-enum SpawnMode {
- #[default]
- Launch,
- Attach,
-}
-
-impl SpawnMode {
- fn label(&self) -> &'static str {
- match self {
- SpawnMode::Launch => "Launch",
- SpawnMode::Attach => "Attach",
- }
- }
-}
-
-impl From for SpawnMode {
- fn from(request: DebugRequestType) -> Self {
- match request {
- DebugRequestType::Launch(_) => SpawnMode::Launch,
- DebugRequestType::Attach(_) => SpawnMode::Attach,
- }
- }
-}
-
-pub(crate) struct InertState {
- focus_handle: FocusHandle,
- selected_debugger: Option,
- program_editor: Entity,
- cwd_editor: Entity,
- workspace: WeakEntity,
- spawn_mode: SpawnMode,
- popover_handle: PopoverMenuHandle,
-}
-
-impl InertState {
- pub(super) fn new(
- workspace: WeakEntity,
- default_cwd: &str,
- debug_config: Option,
- window: &mut Window,
- cx: &mut Context,
- ) -> Self {
- let selected_debugger = debug_config
- .as_ref()
- .map(|config| SharedString::from(config.adapter.clone()));
-
- let spawn_mode = debug_config
- .as_ref()
- .map(|config| config.request.clone().into())
- .unwrap_or_default();
-
- let program = debug_config
- .as_ref()
- .and_then(|config| match &config.request {
- DebugRequestType::Attach(_) => None,
- DebugRequestType::Launch(launch_config) => Some(launch_config.program.clone()),
- });
-
- let program_editor = cx.new(|cx| {
- let mut editor = Editor::single_line(window, cx);
- if let Some(program) = program {
- editor.insert(&program, window, cx);
- } else {
- editor.set_placeholder_text("Program path", cx);
- }
- editor
- });
-
- let cwd = debug_config
- .and_then(|config| match &config.request {
- DebugRequestType::Attach(_) => None,
- DebugRequestType::Launch(launch_config) => launch_config.cwd.clone(),
- })
- .unwrap_or_else(|| PathBuf::from(default_cwd));
-
- let cwd_editor = cx.new(|cx| {
- let mut editor = Editor::single_line(window, cx);
- editor.insert(cwd.to_str().unwrap_or_else(|| default_cwd), window, cx);
- editor.set_placeholder_text("Working directory", cx);
- editor
- });
-
- Self {
- workspace,
- cwd_editor,
- program_editor,
- selected_debugger,
- spawn_mode,
- focus_handle: cx.focus_handle(),
- popover_handle: Default::default(),
- }
- }
-}
-impl Focusable for InertState {
- fn focus_handle(&self, _cx: &App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-pub(crate) enum InertEvent {
- Spawned { config: DebugTaskDefinition },
-}
-
-impl EventEmitter for InertState {}
-
-static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
-
-impl Render for InertState {
- fn render(
- &mut self,
- window: &mut ui::Window,
- cx: &mut ui::Context<'_, Self>,
- ) -> impl ui::IntoElement {
- let weak = cx.weak_entity();
- let workspace = self.workspace.clone();
- let disable_buttons = self.selected_debugger.is_none();
- let spawn_button = ButtonLike::new_rounded_left("spawn-debug-session")
- .child(Label::new(self.spawn_mode.label()).size(LabelSize::Small))
- .on_click(cx.listener(|this, _, window, cx| {
- if this.spawn_mode == SpawnMode::Launch {
- let program = this.program_editor.read(cx).text(cx);
- let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
- let kind = this
- .selected_debugger
- .as_deref()
- .unwrap_or_else(|| {
- unimplemented!(
- "Automatic selection of a debugger based on users project"
- )
- })
- .to_string();
-
- cx.emit(InertEvent::Spawned {
- config: DebugTaskDefinition {
- label: "hard coded".into(),
- adapter: kind,
- request: DebugRequestType::Launch(LaunchConfig {
- program,
- cwd: Some(cwd),
- args: Default::default(),
- }),
- tcp_connection: Some(TCPHost::default()),
- initialize_args: None,
- locator: None,
- stop_on_entry: None,
- },
- });
- } else {
- this.attach(window, cx)
- }
- }))
- .disabled(disable_buttons);
-
- v_flex()
- .track_focus(&self.focus_handle)
- .size_full()
- .gap_1()
- .p_2()
- .child(
- v_flex()
- .gap_1()
- .child(
- h_flex()
- .w_full()
- .gap_2()
- .child(Self::render_editor(&self.program_editor, cx))
- .child(
- h_flex().child(DropdownMenu::new(
- "dap-adapter-picker",
- self.selected_debugger
- .as_ref()
- .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
- .clone(),
- ContextMenu::build(window, cx, move |mut this, _, cx| {
- let setter_for_name = |name: SharedString| {
- let weak = weak.clone();
- move |_: &mut Window, cx: &mut App| {
- let name = name.clone();
- weak.update(cx, move |this, cx| {
- this.selected_debugger = Some(name.clone());
- cx.notify();
- })
- .ok();
- }
- };
- let available_adapters = workspace
- .update(cx, |this, cx| {
- this.project()
- .read(cx)
- .debug_adapters()
- .enumerate_adapters()
- })
- .ok()
- .unwrap_or_default();
-
- for adapter in available_adapters {
- this = this.entry(
- adapter.0.clone(),
- None,
- setter_for_name(adapter.0.clone()),
- );
- }
- this
- }),
- )),
- ),
- )
- .child(
- h_flex()
- .gap_2()
- .child(Self::render_editor(&self.cwd_editor, cx))
- .map(|this| {
- let entity = cx.weak_entity();
- this.child(SplitButton {
- left: spawn_button,
- right: PopoverMenu::new("debugger-select-spawn-mode")
- .trigger(
- ButtonLike::new_rounded_right(
- "debugger-spawn-button-mode",
- )
- .layer(ui::ElevationIndex::ModalSurface)
- .size(ui::ButtonSize::None)
- .child(
- div().px_1().child(
- Icon::new(IconName::ChevronDownSmall)
- .size(IconSize::XSmall),
- ),
- ),
- )
- .menu(move |window, cx| {
- Some(ContextMenu::build(window, cx, {
- let entity = entity.clone();
- move |this, _, _| {
- this.entry("Launch", None, {
- let entity = entity.clone();
- move |_, cx| {
- let _ =
- entity.update(cx, |this, cx| {
- this.spawn_mode =
- SpawnMode::Launch;
- cx.notify();
- });
- }
- })
- .entry("Attach", None, {
- let entity = entity.clone();
- move |_, cx| {
- let _ =
- entity.update(cx, |this, cx| {
- this.spawn_mode =
- SpawnMode::Attach;
- cx.notify();
- });
- }
- })
- }
- }))
- })
- .with_handle(self.popover_handle.clone())
- .into_any_element(),
- })
- }),
- ),
- )
- }
-}
-
-impl InertState {
- fn render_editor(editor: &Entity, cx: &Context) -> impl IntoElement {
- let settings = ThemeSettings::get_global(cx);
- let text_style = TextStyle {
- color: cx.theme().colors().text,
- font_family: settings.buffer_font.family.clone(),
- font_features: settings.buffer_font.features.clone(),
- font_size: settings.buffer_font_size(cx).into(),
- font_weight: settings.buffer_font.weight,
- line_height: relative(settings.buffer_line_height.value()),
- ..Default::default()
- };
-
- EditorElement::new(
- editor,
- EditorStyle {
- background: cx.theme().colors().editor_background,
- local_player: cx.theme().players().local(),
- text: text_style,
- ..Default::default()
- },
- )
- }
-
- fn attach(&self, window: &mut Window, cx: &mut Context) {
- let kind = self
- .selected_debugger
- .as_deref()
- .map(|s| s.to_string())
- .unwrap_or_else(|| {
- unimplemented!("Automatic selection of a debugger based on users project")
- });
-
- let config = DebugTaskDefinition {
- label: "hard coded attach".into(),
- adapter: kind,
- request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
- initialize_args: None,
- locator: None,
- tcp_connection: Some(TCPHost::default()),
- stop_on_entry: None,
- };
-
- let _ = self.workspace.update(cx, |workspace, cx| {
- let project = workspace.project().clone();
- workspace.toggle_modal(window, cx, |window, cx| {
- AttachModal::new(project, config, window, cx)
- });
- });
- }
-}
diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs
index 28137b89fb..9c444796d3 100644
--- a/crates/debugger_ui/src/session/running.rs
+++ b/crates/debugger_ui/src/session/running.rs
@@ -15,10 +15,9 @@ use rpc::proto::ViewId;
use settings::Settings;
use stack_frame_list::StackFrameList;
use ui::{
- ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context, ContextMenu,
- Disableable, Divider, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, Indicator,
- InteractiveElement, IntoElement, Label, ParentElement, Render, SharedString,
- StatefulInteractiveElement, Styled, Tooltip, Window, div, h_flex, v_flex,
+ ActiveTheme, AnyElement, App, Button, Context, ContextMenu, DropdownMenu, FluentBuilder,
+ Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
+ StatefulInteractiveElement, Styled, Window, div, h_flex, v_flex,
};
use util::ResultExt;
use variable_list::VariableList;
@@ -42,7 +41,7 @@ pub struct RunningState {
}
impl Render for RunningState {
- fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement {
+ fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement {
let threads = self.session.update(cx, |this, cx| this.threads(cx));
self.select_current_thread(&threads, cx);
@@ -51,255 +50,27 @@ impl Render for RunningState {
.map(|thread_id| self.session.read(cx).thread_status(thread_id))
.unwrap_or(ThreadStatus::Exited);
- let selected_thread_name = threads
- .iter()
- .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
- .map(|(thread, _)| thread.name.clone())
- .unwrap_or("Threads".to_owned());
-
self.variable_list.update(cx, |this, cx| {
this.disabled(thread_status != ThreadStatus::Stopped, cx);
});
let active_thread_item = &self.active_thread_item;
- let has_no_threads = threads.is_empty();
let capabilities = self.capabilities(cx);
- let state = cx.entity();
h_flex()
.key_context("DebugPanelItem")
.track_focus(&self.focus_handle(cx))
.size_full()
.items_start()
.child(
- v_flex()
- .size_full()
- .items_start()
- .child(
- h_flex()
- .w_full()
- .border_b_1()
- .border_color(cx.theme().colors().border_variant)
- .justify_between()
- .child(
- h_flex()
- .px_1()
- .py_0p5()
- .w_full()
- .gap_1()
- .map(|this| {
- if thread_status == ThreadStatus::Running {
- this.child(
- IconButton::new(
- "debug-pause",
- IconName::DebugPause,
- )
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.pause_thread(cx);
- }))
- .tooltip(move |window, cx| {
- Tooltip::text("Pause program")(window, cx)
- }),
- )
- } else {
- this.child(
- IconButton::new(
- "debug-continue",
- IconName::DebugContinue,
- )
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.continue_thread(cx)
- }))
- .disabled(thread_status != ThreadStatus::Stopped)
- .tooltip(move |window, cx| {
- Tooltip::text("Continue program")(window, cx)
- }),
- )
- }
- })
- .child(
- IconButton::new("debug-restart", IconName::DebugRestart)
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.restart_session(cx);
- }))
- .disabled(
- !capabilities
- .supports_restart_request
- .unwrap_or_default(),
- )
- .tooltip(move |window, cx| {
- Tooltip::text("Restart")(window, cx)
- }),
- )
- .child(
- IconButton::new("debug-stop", IconName::DebugStop)
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.stop_thread(cx);
- }))
- .disabled(
- thread_status != ThreadStatus::Stopped
- && thread_status != ThreadStatus::Running,
- )
- .tooltip({
- let label = if capabilities
- .supports_terminate_threads_request
- .unwrap_or_default()
- {
- "Terminate Thread"
- } else {
- "Terminate all Threads"
- };
- move |window, cx| Tooltip::text(label)(window, cx)
- }),
- )
- .child(
- IconButton::new(
- "debug-disconnect",
- IconName::DebugDisconnect,
- )
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.disconnect_client(cx);
- }))
- .disabled(
- thread_status == ThreadStatus::Exited
- || thread_status == ThreadStatus::Ended,
- )
- .tooltip(Tooltip::text("Disconnect")),
- )
- .child(Divider::vertical())
- .when(
- capabilities.supports_step_back.unwrap_or(false),
- |this| {
- this.child(
- IconButton::new(
- "debug-step-back",
- IconName::DebugStepBack,
- )
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.step_back(cx);
- }))
- .disabled(thread_status != ThreadStatus::Stopped)
- .tooltip(move |window, cx| {
- Tooltip::text("Step back")(window, cx)
- }),
- )
- },
- )
- .child(
- IconButton::new("debug-step-over", IconName::DebugStepOver)
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.step_over(cx);
- }))
- .disabled(thread_status != ThreadStatus::Stopped)
- .tooltip(move |window, cx| {
- Tooltip::text("Step over")(window, cx)
- }),
- )
- .child(
- IconButton::new("debug-step-in", IconName::DebugStepInto)
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.step_in(cx);
- }))
- .disabled(thread_status != ThreadStatus::Stopped)
- .tooltip(move |window, cx| {
- Tooltip::text("Step in")(window, cx)
- }),
- )
- .child(
- IconButton::new("debug-step-out", IconName::DebugStepOut)
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.step_out(cx);
- }))
- .disabled(thread_status != ThreadStatus::Stopped)
- .tooltip(move |window, cx| {
- Tooltip::text("Step out")(window, cx)
- }),
- )
- .child(Divider::vertical())
- .child(
- IconButton::new(
- "debug-ignore-breakpoints",
- if self.session.read(cx).breakpoints_enabled() {
- IconName::DebugBreakpoint
- } else {
- IconName::DebugIgnoreBreakpoints
- },
- )
- .icon_size(IconSize::XSmall)
- .on_click(cx.listener(|this, _, _window, cx| {
- this.toggle_ignore_breakpoints(cx);
- }))
- .disabled(
- thread_status == ThreadStatus::Exited
- || thread_status == ThreadStatus::Ended,
- )
- .tooltip(
- move |window, cx| {
- Tooltip::text("Ignore breakpoints")(window, cx)
- },
- ),
- ),
- )
- .child(
- h_flex()
- .px_1()
- .py_0p5()
- .gap_2()
- .w_3_4()
- .justify_end()
- .child(Label::new("Thread:"))
- .child(
- DropdownMenu::new(
- ("thread-list", self.session_id.0),
- selected_thread_name,
- ContextMenu::build(
- window,
- cx,
- move |mut this, _, _| {
- for (thread, _) in threads {
- let state = state.clone();
- let thread_id = thread.id;
- this = this.entry(
- thread.name,
- None,
- move |_, cx| {
- state.update(cx, |state, cx| {
- state.select_thread(
- ThreadId(thread_id),
- cx,
- );
- });
- },
- );
- }
- this
- },
- ),
- )
- .disabled(
- has_no_threads
- || thread_status != ThreadStatus::Stopped,
- ),
- ),
- ),
- )
- .child(
- h_flex()
- .size_full()
- .items_start()
- .p_1()
- .gap_4()
- .child(self.stack_frame_list.clone()),
- ),
+ v_flex().size_full().items_start().child(
+ h_flex()
+ .size_full()
+ .items_start()
+ .p_1()
+ .gap_4()
+ .child(self.stack_frame_list.clone()),
+ ),
)
.child(
v_flex()
@@ -450,37 +221,32 @@ impl RunningState {
self.session_id
}
- #[cfg(any(test, feature = "test-support"))]
+ #[cfg(test)]
pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context) {
self.active_thread_item = thread_item;
cx.notify()
}
- #[cfg(any(test, feature = "test-support"))]
+ #[cfg(test)]
pub fn stack_frame_list(&self) -> &Entity {
&self.stack_frame_list
}
- #[cfg(any(test, feature = "test-support"))]
+ #[cfg(test)]
pub fn console(&self) -> &Entity {
&self.console
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn module_list(&self) -> &Entity {
+ #[cfg(test)]
+ pub(crate) fn module_list(&self) -> &Entity {
&self.module_list
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn variable_list(&self) -> &Entity {
+ #[cfg(test)]
+ pub(crate) fn variable_list(&self) -> &Entity {
&self.variable_list
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn are_breakpoints_ignored(&self, cx: &App) -> bool {
- self.session.read(cx).ignore_breakpoints()
- }
-
pub fn capabilities(&self, cx: &App) -> Capabilities {
self.session().read(cx).capabilities().clone()
}
@@ -504,8 +270,8 @@ impl RunningState {
}
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn selected_thread_id(&self) -> Option {
+ #[cfg(test)]
+ pub(crate) fn selected_thread_id(&self) -> Option {
self.thread_id
}
@@ -583,7 +349,7 @@ impl RunningState {
});
}
- pub fn step_in(&mut self, cx: &mut Context) {
+ pub(crate) fn step_in(&mut self, cx: &mut Context) {
let Some(thread_id) = self.thread_id else {
return;
};
@@ -595,7 +361,7 @@ impl RunningState {
});
}
- pub fn step_out(&mut self, cx: &mut Context) {
+ pub(crate) fn step_out(&mut self, cx: &mut Context) {
let Some(thread_id) = self.thread_id else {
return;
};
@@ -607,7 +373,7 @@ impl RunningState {
});
}
- pub fn step_back(&mut self, cx: &mut Context) {
+ pub(crate) fn step_back(&mut self, cx: &mut Context) {
let Some(thread_id) = self.thread_id else {
return;
};
@@ -675,6 +441,10 @@ impl RunningState {
});
}
+ #[expect(
+ unused,
+ reason = "Support for disconnecting a client is not wired through yet"
+ )]
pub fn disconnect_client(&self, cx: &mut Context) {
self.session().update(cx, |state, cx| {
state.disconnect_client(cx);
@@ -686,6 +456,36 @@ impl RunningState {
session.toggle_ignore_breakpoints(cx).detach();
});
}
+
+ pub(crate) fn thread_dropdown(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<'_, RunningState>,
+ ) -> DropdownMenu {
+ let state = cx.entity();
+ let threads = self.session.update(cx, |this, cx| this.threads(cx));
+ let selected_thread_name = threads
+ .iter()
+ .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
+ .map(|(thread, _)| thread.name.clone())
+ .unwrap_or("Threads".to_owned());
+ DropdownMenu::new(
+ ("thread-list", self.session_id.0),
+ selected_thread_name,
+ ContextMenu::build(window, cx, move |mut this, _, _| {
+ for (thread, _) in threads {
+ let state = state.clone();
+ let thread_id = thread.id;
+ this = this.entry(thread.name, None, move |_, cx| {
+ state.update(cx, |state, cx| {
+ state.select_thread(ThreadId(thread_id), cx);
+ });
+ });
+ }
+ this
+ }),
+ )
+ }
}
impl EventEmitter for RunningState {}
diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs
index 1f8b9582ff..cccfe68786 100644
--- a/crates/debugger_ui/src/session/running/console.rs
+++ b/crates/debugger_ui/src/session/running/console.rs
@@ -85,16 +85,11 @@ impl Console {
}
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn editor(&self) -> &Entity {
+ #[cfg(test)]
+ pub(crate) fn editor(&self) -> &Entity {
&self.console
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn query_bar(&self) -> &Entity {
- &self.query_bar
- }
-
fn is_local(&self, cx: &Context) -> bool {
self.session.read(cx).is_local()
}
diff --git a/crates/debugger_ui/src/session/running/module_list.rs b/crates/debugger_ui/src/session/running/module_list.rs
index 230c92453c..cf6ce64f62 100644
--- a/crates/debugger_ui/src/session/running/module_list.rs
+++ b/crates/debugger_ui/src/session/running/module_list.rs
@@ -147,11 +147,9 @@ impl ModuleList {
)
.into_any()
}
-}
-#[cfg(any(test, feature = "test-support"))]
-impl ModuleList {
- pub fn modules(&self, cx: &mut Context) -> Vec {
+ #[cfg(test)]
+ pub(crate) fn modules(&self, cx: &mut Context) -> Vec {
self.session
.update(cx, |session, cx| session.modules(cx).to_vec())
}
diff --git a/crates/debugger_ui/src/session/running/stack_frame_list.rs b/crates/debugger_ui/src/session/running/stack_frame_list.rs
index 1b24c643e7..8cecf55594 100644
--- a/crates/debugger_ui/src/session/running/stack_frame_list.rs
+++ b/crates/debugger_ui/src/session/running/stack_frame_list.rs
@@ -87,13 +87,13 @@ impl StackFrameList {
}
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn entries(&self) -> &Vec {
+ #[cfg(test)]
+ pub(crate) fn entries(&self) -> &Vec {
&self.entries
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn flatten_entries(&self) -> Vec {
+ #[cfg(test)]
+ pub(crate) fn flatten_entries(&self) -> Vec {
self.entries
.iter()
.flat_map(|frame| match frame {
@@ -115,8 +115,8 @@ impl StackFrameList {
.unwrap_or_default()
}
- #[cfg(any(test, feature = "test-support"))]
- pub fn dap_stack_frames(&self, cx: &mut App) -> Vec {
+ #[cfg(test)]
+ pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec {
self.stack_frames(cx)
.into_iter()
.map(|stack_frame| stack_frame.dap.clone())
diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs
index 6921024d57..2310dcb74f 100644
--- a/crates/debugger_ui/src/session/running/variable_list.rs
+++ b/crates/debugger_ui/src/session/running/variable_list.rs
@@ -540,8 +540,8 @@ impl VariableList {
}
#[track_caller]
- #[cfg(any(test, feature = "test-support"))]
- pub fn assert_visual_entries(&self, expected: Vec<&str>) {
+ #[cfg(test)]
+ pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
const INDENT: &'static str = " ";
let entries = &self.entries;
@@ -569,8 +569,8 @@ impl VariableList {
}
#[track_caller]
- #[cfg(any(test, feature = "test-support"))]
- pub fn scopes(&self) -> Vec {
+ #[cfg(test)]
+ pub(crate) fn scopes(&self) -> Vec {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
@@ -582,8 +582,8 @@ impl VariableList {
}
#[track_caller]
- #[cfg(any(test, feature = "test-support"))]
- pub fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec)> {
+ #[cfg(test)]
+ pub(crate) fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec)> {
let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
let mut idx = 0;
@@ -604,8 +604,8 @@ impl VariableList {
}
#[track_caller]
- #[cfg(any(test, feature = "test-support"))]
- pub fn variables(&self) -> Vec {
+ #[cfg(test)]
+ pub(crate) fn variables(&self) -> Vec {
self.entries
.iter()
.filter_map(|entry| match &entry.dap_kind {
diff --git a/crates/debugger_ui/src/session/starting.rs b/crates/debugger_ui/src/session/starting.rs
deleted file mode 100644
index bdbeee6302..0000000000
--- a/crates/debugger_ui/src/session/starting.rs
+++ /dev/null
@@ -1,80 +0,0 @@
-use std::time::Duration;
-
-use anyhow::Result;
-
-use dap::client::SessionId;
-use gpui::{
- Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task, Transformation,
- percentage,
-};
-use project::debugger::session::Session;
-use ui::{Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled, v_flex};
-
-pub(crate) struct StartingState {
- focus_handle: FocusHandle,
- pub(super) session_id: SessionId,
- _notify_parent: Task<()>,
-}
-
-pub(crate) enum StartingEvent {
- Failed,
- Finished(Entity),
-}
-
-impl EventEmitter for StartingState {}
-
-impl StartingState {
- pub(crate) fn new(
- session_id: SessionId,
- task: Task>>,
- cx: &mut Context,
- ) -> Self {
- let _notify_parent = cx.spawn(async move |this, cx| {
- let entity = task.await;
-
- this.update(cx, |_, cx| {
- if let Ok(entity) = entity {
- cx.emit(StartingEvent::Finished(entity))
- } else {
- cx.emit(StartingEvent::Failed)
- }
- })
- .ok();
- });
- Self {
- session_id,
- focus_handle: cx.focus_handle(),
- _notify_parent,
- }
- }
-}
-
-impl Focusable for StartingState {
- fn focus_handle(&self, _: &ui::App) -> FocusHandle {
- self.focus_handle.clone()
- }
-}
-
-impl Render for StartingState {
- fn render(
- &mut self,
- _window: &mut ui::Window,
- _cx: &mut ui::Context<'_, Self>,
- ) -> impl ui::IntoElement {
- v_flex()
- .size_full()
- .gap_1()
- .items_center()
- .child("Starting a debug adapter")
- .child(
- Icon::new(IconName::ArrowCircle)
- .color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
- )
- .into_any_element(),
- )
- }
-}
diff --git a/crates/debugger_ui/src/tests/attach_modal.rs b/crates/debugger_ui/src/tests/attach_modal.rs
index 75b3e78b77..868191f22d 100644
--- a/crates/debugger_ui/src/tests/attach_modal.rs
+++ b/crates/debugger_ui/src/tests/attach_modal.rs
@@ -109,6 +109,7 @@ async fn test_show_attach_modal_and_select_process(
command: vec![],
},
],
+ true,
window,
cx,
)
diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs
index 4ddff9701d..48722676f5 100644
--- a/crates/debugger_ui/src/tests/debugger_panel.rs
+++ b/crates/debugger_ui/src/tests/debugger_panel.rs
@@ -99,8 +99,8 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
debug_panel.update(cx, |this, cx| {
assert!(this.active_session(cx).is_some());
- // we have one active session and one inert item
- assert_eq!(2, this.pane().unwrap().read(cx).items_len());
+ // we have one active session
+ assert_eq!(1, this.pane().unwrap().read(cx).items_len());
assert!(running_state.read(cx).selected_thread_id().is_none());
});
})
@@ -135,9 +135,9 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
.clone()
});
- // we have one active session and one inert item
+ // we have one active session
assert_eq!(
- 2,
+ 1,
debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
);
assert_eq!(client.id(), running_state.read(cx).session_id());
@@ -175,7 +175,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
debug_panel.update(cx, |this, cx| {
assert!(this.active_session(cx).is_some());
- assert_eq!(2, this.pane().unwrap().read(cx).items_len());
+ assert_eq!(1, this.pane().unwrap().read(cx).items_len());
assert_eq!(
ThreadId(1),
running_state.read(cx).selected_thread_id().unwrap()
@@ -245,8 +245,8 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
debug_panel.update(cx, |this, cx| {
assert!(this.active_session(cx).is_some());
- // we have one active session and one inert item
- assert_eq!(2, this.pane().unwrap().read(cx).items_len());
+ // we have one active session
+ assert_eq!(1, this.pane().unwrap().read(cx).items_len());
});
})
.unwrap();
@@ -281,9 +281,9 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
.clone()
});
- // we have one active session and one inert item
+ // we have one active session
assert_eq!(
- 2,
+ 1,
debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
);
assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
@@ -323,9 +323,9 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
.clone()
});
- // we have one active session and one inert item
+ // we have one active session
assert_eq!(
- 2,
+ 1,
debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
);
assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
@@ -362,7 +362,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
debug_panel.update(cx, |this, cx| {
assert!(this.active_session(cx).is_some());
- assert_eq!(2, this.pane().unwrap().read(cx).items_len());
+ assert_eq!(1, this.pane().unwrap().read(cx).items_len());
assert_eq!(
ThreadId(1),
running_state.read(cx).selected_thread_id().unwrap()
@@ -1447,7 +1447,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
})
.await;
- cx.dispatch_action(workspace::ClearAllBreakpoints);
+ cx.dispatch_action(crate::ClearAllBreakpoints);
cx.run_until_parked();
let shutdown_session = project.update(cx, |project, cx| {
diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs
index f6ec393783..4dab811fd6 100644
--- a/crates/icons/src/icons.rs
+++ b/crates/icons/src/icons.rs
@@ -23,6 +23,7 @@ pub enum IconName {
ArrowCircle,
ArrowDown,
ArrowDownFromLine,
+ ArrowDownRight,
ArrowLeft,
ArrowRight,
ArrowRightLeft,
@@ -44,6 +45,7 @@ pub enum IconName {
BookCopy,
BookPlus,
Brain,
+ BugOff,
CaseSensitive,
Check,
CheckDouble,
@@ -55,6 +57,7 @@ pub enum IconName {
ChevronUp,
ChevronUpDown,
Circle,
+ CircleOff,
Clipboard,
Close,
Code,
@@ -166,6 +169,7 @@ pub enum IconName {
Play,
Plus,
PocketKnife,
+ Power,
Public,
PullRequest,
Quote,
diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs
index 8df299f125..3a90b68f5c 100644
--- a/crates/project/src/debugger/dap_store.rs
+++ b/crates/project/src/debugger/dap_store.rs
@@ -48,6 +48,7 @@ use worktree::Worktree;
pub enum DapStoreEvent {
DebugClientStarted(SessionId),
+ DebugSessionInitialized(SessionId),
DebugClientShutdown(SessionId),
DebugClientEvent {
session_id: SessionId,
@@ -862,6 +863,10 @@ fn create_new_session(
}
}
+ this.update(cx, |_, cx| {
+ cx.emit(DapStoreEvent::DebugSessionInitialized(session_id));
+ })?;
+
Ok(session)
});
task
diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs
index ac10fecbfb..6d205e70bb 100644
--- a/crates/project/src/debugger/session.rs
+++ b/crates/project/src/debugger/session.rs
@@ -537,7 +537,11 @@ impl LocalMode {
Ok((adapter, binary))
}
- pub fn initialize_sequence(
+ pub fn label(&self) -> String {
+ self.config.label.clone()
+ }
+
+ fn initialize_sequence(
&self,
capabilities: &Capabilities,
initialized_rx: oneshot::Receiver<()>,
diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs
index 1a303a9943..ded01eda58 100644
--- a/crates/project/src/task_inventory.rs
+++ b/crates/project/src/task_inventory.rs
@@ -125,6 +125,22 @@ impl Inventory {
cx.new(|_| Self::default())
}
+ pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> {
+ self.templates_from_settings
+ .worktree
+ .values()
+ .flat_map(|tasks| {
+ tasks.iter().filter_map(|(kind, tasks)| {
+ if matches!(kind.1, TaskKind::Debug) {
+ Some(tasks)
+ } else {
+ None
+ }
+ })
+ })
+ .flatten()
+ .collect()
+ }
/// Pulls its task sources relevant to the worktree and the language given,
/// returns all task templates with their source kinds, worktree tasks first, language tasks second
/// and global tasks last. No specific order inside source kinds groups.
diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs
index 3a62e10ebc..7634b7bfb8 100644
--- a/crates/task/src/debug_format.rs
+++ b/crates/task/src/debug_format.rs
@@ -41,7 +41,6 @@ impl TCPHost {
#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
pub struct AttachConfig {
/// The processId to attach to, if left empty we will show a process picker
- #[serde(default)]
pub process_id: Option,
}
@@ -52,7 +51,8 @@ pub struct LaunchConfig {
pub program: String,
/// The current working directory of your project
pub cwd: Option,
- /// Args to pass to a debuggee
+ /// Arguments to pass to a debuggee
+ #[serde(default)]
pub args: Vec,
}
@@ -66,6 +66,17 @@ pub enum DebugRequestType {
Attach(AttachConfig),
}
+impl From for DebugRequestType {
+ fn from(launch_config: LaunchConfig) -> Self {
+ DebugRequestType::Launch(launch_config)
+ }
+}
+
+impl From for DebugRequestType {
+ fn from(attach_config: AttachConfig) -> Self {
+ DebugRequestType::Attach(attach_config)
+ }
+}
/// Represents a request for starting the debugger.
/// Contrary to `DebugRequestType`, `DebugRequestDisposition` is not Serializable.
#[derive(PartialEq, Eq, Clone, Debug)]
@@ -144,6 +155,37 @@ impl TryFrom for DebugTaskDefinition {
}
}
+impl TryFrom for DebugTaskDefinition {
+ type Error = ();
+
+ fn try_from(value: TaskTemplate) -> Result {
+ let TaskType::Debug(debug_args) = value.task_type else {
+ return Err(());
+ };
+
+ let request = match debug_args.request {
+ crate::DebugArgsRequest::Launch => DebugRequestType::Launch(LaunchConfig {
+ program: value.command,
+ cwd: value.cwd.map(PathBuf::from),
+ args: value.args,
+ }),
+ crate::DebugArgsRequest::Attach(attach_config) => {
+ DebugRequestType::Attach(attach_config)
+ }
+ };
+
+ Ok(DebugTaskDefinition {
+ adapter: debug_args.adapter,
+ request,
+ label: value.label,
+ initialize_args: debug_args.initialize_args,
+ tcp_connection: debug_args.tcp_connection,
+ locator: debug_args.locator,
+ stop_on_entry: debug_args.stop_on_entry,
+ })
+ }
+}
+
impl DebugTaskDefinition {
/// Translate from debug definition to a task template
pub fn to_zed_format(self) -> anyhow::Result {
@@ -249,3 +291,21 @@ impl TryFrom for TaskTemplates {
Ok(Self(templates))
}
}
+
+#[cfg(test)]
+mod tests {
+ use crate::{DebugRequestType, LaunchConfig};
+
+ #[test]
+ fn test_can_deserialize_non_attach_task() {
+ let deserialized: DebugRequestType =
+ serde_json::from_str(r#"{"program": "cafebabe"}"#).unwrap();
+ assert_eq!(
+ deserialized,
+ DebugRequestType::Launch(LaunchConfig {
+ program: "cafebabe".to_owned(),
+ ..Default::default()
+ })
+ );
+ }
+}
diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs
index 77a1db5372..257bb0bb8b 100644
--- a/crates/tasks_ui/src/modal.rs
+++ b/crates/tasks_ui/src/modal.rs
@@ -338,6 +338,7 @@ impl PickerDelegate for TasksModalDelegate {
debugger_ui::attach_modal::AttachModal::new(
project,
config.clone(),
+ true,
window,
cx,
)
diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs
index 016f231b1a..ed84c73549 100644
--- a/crates/tasks_ui/src/tasks_ui.rs
+++ b/crates/tasks_ui/src/tasks_ui.rs
@@ -1,6 +1,7 @@
use std::collections::HashMap;
use std::path::Path;
+use debugger_ui::Start;
use editor::Editor;
use feature_flags::{Debugger, FeatureFlagViewExt};
use gpui::{App, AppContext as _, Context, Entity, Task, Window};
@@ -8,7 +9,7 @@ use modal::{TaskOverrides, TasksModal};
use project::{Location, TaskContexts, Worktree};
use task::{RevealTarget, TaskContext, TaskId, TaskModal, TaskVariables, VariableName};
use workspace::tasks::schedule_task;
-use workspace::{Start, Workspace, tasks::schedule_resolved_task};
+use workspace::{Workspace, tasks::schedule_resolved_task};
mod modal;
diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs
index 5cfccd8246..990c9c8c84 100644
--- a/crates/ui/src/components/button/toggle_button.rs
+++ b/crates/ui/src/components/button/toggle_button.rs
@@ -71,6 +71,18 @@ impl SelectableButton for ToggleButton {
}
}
+impl FixedWidth for ToggleButton {
+ fn width(mut self, width: DefiniteLength) -> Self {
+ self.base.width = Some(width);
+ self
+ }
+
+ fn full_width(mut self) -> Self {
+ self.base.width = Some(relative(1.));
+ self
+ }
+}
+
impl Disableable for ToggleButton {
fn disabled(mut self, disabled: bool) -> Self {
self.base = self.base.disabled(disabled);
diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs
index debd97f715..f9c62e3c2c 100644
--- a/crates/ui/src/components/toggle.rs
+++ b/crates/ui/src/components/toggle.rs
@@ -253,6 +253,7 @@ pub struct CheckboxWithLabel {
on_click: Arc,
filled: bool,
style: ToggleStyle,
+ checkbox_position: IconPosition,
}
// TODO: Remove `CheckboxWithLabel` now that `label` is a method of `Checkbox`.
@@ -271,6 +272,7 @@ impl CheckboxWithLabel {
on_click: Arc::new(on_click),
filled: false,
style: ToggleStyle::default(),
+ checkbox_position: IconPosition::Start,
}
}
@@ -291,31 +293,51 @@ impl CheckboxWithLabel {
self.filled = true;
self
}
+
+ pub fn checkbox_position(mut self, position: IconPosition) -> Self {
+ self.checkbox_position = position;
+ self
+ }
}
impl RenderOnce for CheckboxWithLabel {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
- .child(
- Checkbox::new(self.id.clone(), self.checked)
- .style(self.style)
- .when(self.filled, Checkbox::fill)
- .on_click({
- let on_click = self.on_click.clone();
- move |checked, window, cx| {
- (on_click)(checked, window, cx);
- }
- }),
- )
+ .when(self.checkbox_position == IconPosition::Start, |this| {
+ this.child(
+ Checkbox::new(self.id.clone(), self.checked)
+ .style(self.style.clone())
+ .when(self.filled, Checkbox::fill)
+ .on_click({
+ let on_click = self.on_click.clone();
+ move |checked, window, cx| {
+ (on_click)(checked, window, cx);
+ }
+ }),
+ )
+ })
.child(
div()
.id(SharedString::from(format!("{}-label", self.id)))
- .on_click(move |_event, window, cx| {
- (self.on_click)(&self.checked.inverse(), window, cx);
+ .on_click({
+ let on_click = self.on_click.clone();
+ move |_event, window, cx| {
+ (on_click)(&self.checked.inverse(), window, cx);
+ }
})
.child(self.label),
)
+ .when(self.checkbox_position == IconPosition::End, |this| {
+ this.child(
+ Checkbox::new(self.id.clone(), self.checked)
+ .style(self.style)
+ .when(self.filled, Checkbox::fill)
+ .on_click(move |checked, window, cx| {
+ (self.on_click)(checked, window, cx);
+ }),
+ )
+ })
}
}
diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs
index 209a195a36..813e9ce005 100644
--- a/crates/workspace/src/workspace.rs
+++ b/crates/workspace/src/workspace.rs
@@ -129,24 +129,6 @@ static ZED_WINDOW_POSITION: LazyLock