From ece4a1cd7c7952b99c6d4ded8abcf8b0a4cea64d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 3 Apr 2025 18:11:14 +0200 Subject: [PATCH] debugger: Start on tabless design (#27837) ![image](https://github.com/user-attachments/assets/1cd54b70-5457-4c64-95bd-45a7055ea165) Release Notes: - N/A --------- Co-authored-by: Anthony Eid Co-authored-by: Anthony --- .zed/debug.json | 4 +- assets/icons/arrow_down_right.svg | 1 + assets/icons/arrow_up_right.svg | 4 +- assets/icons/bug_off.svg | 1 + assets/icons/circle_off.svg | 1 + assets/icons/power.svg | 1 + crates/dap_adapters/src/lldb.rs | 2 +- crates/dap_adapters/src/python.rs | 37 +- crates/debugger_ui/src/attach_modal.rs | 5 +- crates/debugger_ui/src/debugger_panel.rs | 429 +++++++++--- crates/debugger_ui/src/debugger_ui.rs | 49 +- crates/debugger_ui/src/new_session_modal.rs | 633 ++++++++++++++++++ crates/debugger_ui/src/session.rs | 202 +----- crates/debugger_ui/src/session/failed.rs | 30 - crates/debugger_ui/src/session/inert.rs | 337 ---------- crates/debugger_ui/src/session/running.rs | 316 ++------- .../src/session/running/console.rs | 9 +- .../src/session/running/module_list.rs | 6 +- .../src/session/running/stack_frame_list.rs | 12 +- .../src/session/running/variable_list.rs | 16 +- crates/debugger_ui/src/session/starting.rs | 80 --- crates/debugger_ui/src/tests/attach_modal.rs | 1 + .../debugger_ui/src/tests/debugger_panel.rs | 26 +- crates/icons/src/icons.rs | 4 + crates/project/src/debugger/dap_store.rs | 5 + crates/project/src/debugger/session.rs | 6 +- crates/project/src/task_inventory.rs | 16 + crates/task/src/debug_format.rs | 64 +- crates/tasks_ui/src/modal.rs | 1 + crates/tasks_ui/src/tasks_ui.rs | 3 +- .../ui/src/components/button/toggle_button.rs | 12 + crates/ui/src/components/toggle.rs | 48 +- crates/workspace/src/workspace.rs | 18 - 33 files changed, 1287 insertions(+), 1092 deletions(-) create mode 100644 assets/icons/arrow_down_right.svg create mode 100644 assets/icons/bug_off.svg create mode 100644 assets/icons/circle_off.svg create mode 100644 assets/icons/power.svg create mode 100644 crates/debugger_ui/src/new_session_modal.rs delete mode 100644 crates/debugger_ui/src/session/failed.rs delete mode 100644 crates/debugger_ui/src/session/inert.rs delete mode 100644 crates/debugger_ui/src/session/starting.rs diff --git a/.zed/debug.json b/.zed/debug.json index b7646ee3bd..edc38f4e7c 100644 --- a/.zed/debug.json +++ b/.zed/debug.json @@ -1,14 +1,14 @@ [ { "label": "Debug Zed with LLDB", - "adapter": "lldb", + "adapter": "LLDB", "program": "$ZED_WORKTREE_ROOT/target/debug/zed", "request": "launch", "cwd": "$ZED_WORKTREE_ROOT" }, { "label": "Debug Zed with GDB", - "adapter": "gdb", + "adapter": "GDB", "program": "$ZED_WORKTREE_ROOT/target/debug/zed", "request": "launch", "cwd": "$ZED_WORKTREE_ROOT", diff --git a/assets/icons/arrow_down_right.svg b/assets/icons/arrow_down_right.svg new file mode 100644 index 0000000000..b9c10263d0 --- /dev/null +++ b/assets/icons/arrow_down_right.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/arrow_up_right.svg b/assets/icons/arrow_up_right.svg index 3712b31ebd..9fbafba4ec 100644 --- a/assets/icons/arrow_up_right.svg +++ b/assets/icons/arrow_up_right.svg @@ -1,3 +1 @@ - - - + diff --git a/assets/icons/bug_off.svg b/assets/icons/bug_off.svg new file mode 100644 index 0000000000..23f4ef06df --- /dev/null +++ b/assets/icons/bug_off.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/circle_off.svg b/assets/icons/circle_off.svg new file mode 100644 index 0000000000..be1bf29225 --- /dev/null +++ b/assets/icons/circle_off.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/power.svg b/assets/icons/power.svg new file mode 100644 index 0000000000..787d1a3519 --- /dev/null +++ b/assets/icons/power.svg @@ -0,0 +1 @@ + diff --git a/crates/dap_adapters/src/lldb.rs b/crates/dap_adapters/src/lldb.rs index e6d659eb71..d319f5e3e0 100644 --- a/crates/dap_adapters/src/lldb.rs +++ b/crates/dap_adapters/src/lldb.rs @@ -78,10 +78,10 @@ impl DebugAdapter for LldbDebugAdapter { match &config.request { DebugRequestType::Attach(attach) => { map.insert("pid".into(), attach.process_id.into()); - map.insert("stopOnEntry".into(), config.stop_on_entry.into()); } DebugRequestType::Launch(launch) => { map.insert("program".into(), launch.program.clone().into()); + map.insert("stopOnEntry".into(), config.stop_on_entry.into()); map.insert("args".into(), launch.args.clone().into()); map.insert( "cwd".into(), diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index c7e6daed06..41bf79ff7d 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -126,24 +126,31 @@ impl DebugAdapter for PythonDebugAdapter { } fn request_args(&self, config: &DebugTaskDefinition) -> Value { + let mut args = json!({ + "request": match config.request { + DebugRequestType::Launch(_) => "launch", + DebugRequestType::Attach(_) => "attach", + }, + "subProcess": true, + "redirectOutput": true, + }); + let map = args.as_object_mut().unwrap(); match &config.request { - DebugRequestType::Launch(launch_config) => { - json!({ - "program": launch_config.program, - "args": launch_config.args, - "subProcess": true, - "cwd": launch_config.cwd, - "redirectOutput": true, - "StopOnEntry": config.stop_on_entry, - }) + DebugRequestType::Attach(attach) => { + map.insert("processId".into(), attach.process_id.into()); } - dap::DebugRequestType::Attach(attach_config) => { - json!({ - "subProcess": true, - "redirectOutput": true, - "processId": attach_config.process_id - }) + DebugRequestType::Launch(launch) => { + map.insert("program".into(), launch.program.clone().into()); + map.insert("args".into(), launch.args.clone().into()); + + if let Some(stop_on_entry) = config.stop_on_entry { + map.insert("stopOnEntry".into(), stop_on_entry.into()); + } + if let Some(cwd) = launch.cwd.as_ref() { + map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into()); + } } } + args } } diff --git a/crates/debugger_ui/src/attach_modal.rs b/crates/debugger_ui/src/attach_modal.rs index de4e005939..0ba8efc5e3 100644 --- a/crates/debugger_ui/src/attach_modal.rs +++ b/crates/debugger_ui/src/attach_modal.rs @@ -54,6 +54,7 @@ impl AttachModal { pub fn new( project: Entity, debug_config: task::DebugTaskDefinition, + modal: bool, window: &mut Window, cx: &mut Context, ) -> Self { @@ -74,13 +75,14 @@ impl AttachModal { }) .collect(); processes.sort_by_key(|k| k.name.clone()); - Self::with_processes(project, debug_config, processes, window, cx) + Self::with_processes(project, debug_config, processes, modal, window, cx) } pub(super) fn with_processes( project: Entity, debug_config: task::DebugTaskDefinition, processes: Vec, + modal: bool, window: &mut Window, cx: &mut Context, ) -> Self { @@ -103,6 +105,7 @@ impl AttachModal { window, cx, ) + .modal(modal) }); Self { _subscription: cx.subscribe(&picker, |_, _, _, cx| { diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 0e441f2376..a71841b8a2 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1,4 +1,8 @@ -use crate::session::DebugSession; +use crate::{ + ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack, + StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, +}; +use crate::{new_session_modal::NewSessionModal, session::DebugSession}; use anyhow::{Result, anyhow}; use collections::HashMap; use command_palette_hooks::CommandPaletteFilter; @@ -13,7 +17,10 @@ use gpui::{ }; use project::{ Project, - debugger::dap_store::{self, DapStore}, + debugger::{ + dap_store::{self, DapStore}, + session::ThreadStatus, + }, terminals::TerminalKind, }; use rpc::proto::{self}; @@ -21,11 +28,9 @@ use settings::Settings; use std::{any::TypeId, path::PathBuf}; use task::DebugTaskDefinition; use terminal_view::terminal_panel::TerminalPanel; -use ui::prelude::*; -use util::ResultExt; +use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*}; use workspace::{ - ClearAllBreakpoints, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, - StepOver, Stop, ToggleIgnoreBreakpoints, Workspace, + Pane, Workspace, dock::{DockPosition, Panel, PanelEvent}, pane, }; @@ -51,10 +56,11 @@ actions!(debug_panel, [ToggleFocus]); pub struct DebugPanel { size: Pixels, pane: Entity, + /// This represents the last debug definition that was created in the new session modal + pub(crate) past_debug_definition: Option, project: WeakEntity, workspace: WeakEntity, _subscriptions: Vec, - pub(crate) last_inert_config: Option, } impl DebugPanel { @@ -66,8 +72,6 @@ impl DebugPanel { cx.new(|cx| { let project = workspace.project().clone(); let dap_store = project.read(cx).dap_store(); - let weak_workspace = workspace.weak_handle(); - let debug_panel = cx.weak_entity(); let pane = cx.new(|cx| { let mut pane = Pane::new( workspace.weak_handle(), @@ -81,71 +85,9 @@ impl DebugPanel { pane.set_can_split(None); pane.set_can_navigate(true, cx); pane.display_nav_history_buttons(None); - pane.set_should_display_tab_bar(|_window, _cx| true); + pane.set_should_display_tab_bar(|_window, _cx| false); pane.set_close_pane_if_empty(true, cx); - pane.set_render_tab_bar_buttons(cx, { - let project = project.clone(); - let weak_workspace = weak_workspace.clone(); - let debug_panel = debug_panel.clone(); - move |_, _, cx| { - let project = project.clone(); - let weak_workspace = weak_workspace.clone(); - ( - None, - Some( - h_flex() - .child( - IconButton::new("new-debug-session", IconName::Plus) - .icon_size(IconSize::Small) - .on_click({ - let debug_panel = debug_panel.clone(); - cx.listener(move |pane, _, window, cx| { - let config = debug_panel - .read_with(cx, |this: &DebugPanel, _| { - this.last_inert_config.clone() - }) - .log_err() - .flatten(); - - pane.add_item( - Box::new(DebugSession::inert( - project.clone(), - weak_workspace.clone(), - debug_panel.clone(), - config, - window, - cx, - )), - false, - false, - None, - window, - cx, - ); - }) - }), - ) - .into_any_element(), - ), - ) - } - }); - pane.add_item( - Box::new(DebugSession::inert( - project.clone(), - weak_workspace.clone(), - debug_panel.clone(), - None, - window, - cx, - )), - false, - false, - None, - window, - cx, - ); pane }); @@ -159,7 +101,7 @@ impl DebugPanel { pane, size: px(300.), _subscriptions, - last_inert_config: None, + past_debug_definition: None, project: project.downgrade(), workspace: workspace.weak_handle(), }; @@ -295,7 +237,7 @@ impl DebugPanel { cx: &mut Context, ) { match event { - dap_store::DapStoreEvent::DebugClientStarted(session_id) => { + dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => { let Some(session) = dap_store.read(cx).session_by_id(session_id) else { return log::error!( "Couldn't get session with id: {session_id:?} from DebugClientStarted event" @@ -470,6 +412,274 @@ impl DebugPanel { _ => {} } } + + fn top_controls_strip(&self, window: &mut Window, cx: &mut Context) -> Option
{ + 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>> = LazyLock::new(|| { actions!(assistant, [ShowConfiguration]); -actions!( - debugger, - [ - Start, - Continue, - Disconnect, - Pause, - Restart, - StepInto, - StepOver, - StepOut, - StepBack, - Stop, - ToggleIgnoreBreakpoints, - ClearAllBreakpoints - ] -); - actions!( workspace, [