diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b1cc860663..11dd116c8c 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -12,8 +12,8 @@ use dap::{ }; use futures::{SinkExt as _, channel::mpsc}; use gpui::{ - Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - Subscription, Task, WeakEntity, actions, + Action, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, FocusHandle, + Focusable, Subscription, Task, WeakEntity, actions, }; use project::{ Project, @@ -336,6 +336,95 @@ impl DebugPanel { }) } + fn close_session(&mut self, entity_id: EntityId, cx: &mut Context) { + let Some(session) = self + .sessions + .iter() + .find(|other| entity_id == other.entity_id()) + else { + return; + }; + + session.update(cx, |session, cx| session.shutdown(cx)); + + self.sessions.retain(|other| entity_id != other.entity_id()); + + if let Some(active_session_id) = self + .active_session + .as_ref() + .map(|session| session.entity_id()) + { + if active_session_id == entity_id { + self.active_session = self.sessions.first().cloned(); + } + } + } + + fn sessions_drop_down_menu( + &self, + active_session: &Entity, + window: &mut Window, + cx: &mut Context, + ) -> DropdownMenu { + let sessions = self.sessions.clone(); + let weak = cx.weak_entity(); + let label = active_session.read(cx).label_element(cx); + + DropdownMenu::new_with_element( + "debugger-session-list", + label, + ContextMenu::build(window, cx, move |mut this, _, _| { + for session in sessions.into_iter() { + let weak_session = session.downgrade(); + let weak_id = weak_session.entity_id(); + + this = this.custom_entry( + { + let weak = weak.clone(); + move |_, cx| { + weak_session + .read_with(cx, |session, cx| { + h_flex() + .w_full() + .justify_between() + .child(session.label_element(cx)) + .child( + IconButton::new( + "close-debug-session", + IconName::Close, + ) + .icon_size(IconSize::Small) + .on_click({ + let weak = weak.clone(); + move |_, _, cx| { + weak.update(cx, |panel, cx| { + panel.close_session(weak_id, cx); + }) + .ok(); + } + }), + ) + .into_any_element() + }) + .unwrap_or_else(|_| div().into_any_element()) + } + }, + { + let weak = weak.clone(); + move |window, cx| { + weak.update(cx, |panel, cx| { + panel.activate_session(session.clone(), window, cx); + }) + .ok(); + } + }, + ); + } + this + }), + ) + } + fn top_controls_strip(&self, window: &mut Window, cx: &mut Context) -> Option
{ let active_session = self.active_session.clone(); @@ -529,34 +618,8 @@ impl DebugPanel { }, ) .when_some(active_session.as_ref(), |this, session| { - let sessions = self.sessions.clone(); - let weak = cx.weak_entity(); - let label = session.read(cx).label(cx); - this.child(DropdownMenu::new( - "debugger-session-list", - label, - ContextMenu::build(window, cx, move |mut this, _, cx| { - for item in sessions { - let weak = weak.clone(); - this = this.entry( - session.read(cx).label(cx), - None, - move |window, cx| { - weak.update(cx, |panel, cx| { - panel.activate_session( - item.clone(), - window, - cx, - ); - }) - .ok(); - }, - ); - } - this - }), - )) - .child(Divider::vertical()) + let context_menu = self.sessions_drop_down_menu(session, window, cx); + this.child(context_menu).child(Divider::vertical()) }) .child( IconButton::new("debug-new-session", IconName::Plus) diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index ddda3a2c69..038478a9db 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -7,7 +7,7 @@ use project::debugger::{dap_store::DapStore, session::Session}; use project::worktree_store::WorktreeStore; use rpc::proto::{self, PeerId}; use running::RunningState; -use ui::prelude::*; +use ui::{Indicator, prelude::*}; use workspace::{ FollowableItem, ViewId, Workspace, item::{self, Item}, @@ -81,7 +81,6 @@ impl DebugSession { } } - #[expect(unused)] pub(crate) fn shutdown(&mut self, cx: &mut Context) { match &self.mode { DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)), @@ -108,6 +107,33 @@ impl DebugSession { .expect("Remote Debug Sessions are not implemented yet") .label() } + + pub(crate) fn label_element(&self, cx: &App) -> AnyElement { + let label = self.label(cx); + + let (icon, color) = match &self.mode { + DebugSessionState::Running(state) => { + if state.read(cx).session().read(cx).is_terminated() { + (Some(Indicator::dot().color(Color::Error)), Color::Error) + } else { + match state.read(cx).thread_status(cx).unwrap_or_default() { + project::debugger::session::ThreadStatus::Stopped => ( + Some(Indicator::dot().color(Color::Conflict)), + Color::Conflict, + ), + _ => (Some(Indicator::dot().color(Color::Success)), Color::Success), + } + } + } + }; + + h_flex() + .gap_2() + .when_some(icon, |this, indicator| this.child(indicator)) + .justify_between() + .child(Label::new(label).color(color)) + .into_any_element() + } } impl EventEmitter for DebugSession {} diff --git a/crates/markdown/src/parser.rs b/crates/markdown/src/parser.rs index 5d80b58041..05ba1238f8 100644 --- a/crates/markdown/src/parser.rs +++ b/crates/markdown/src/parser.rs @@ -219,7 +219,7 @@ pub enum MarkdownEvent { Start(MarkdownTag), /// End of a tagged element. End(MarkdownTagEnd), - /// Text that uses the associated range from the mardown source. + /// Text that uses the associated range from the markdown source. Text, /// Text that differs from the markdown source - typically due to substitution of HTML entities /// and smart punctuation. diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 7f266f8c41..04efd4c29f 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -1,7 +1,7 @@ use super::{ breakpoint_store::BreakpointStore, locator_store::LocatorStore, - session::{self, Session}, + session::{self, Session, SessionStateEvent}, }; use crate::{ProjectEnvironment, debugger, worktree_store::WorktreeStore}; use anyhow::{Result, anyhow}; @@ -869,6 +869,15 @@ fn create_new_session( } this.update(cx, |_, cx| { + cx.subscribe( + &session, + move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event { + SessionStateEvent::Shutdown => { + this.shutdown_session(session_id, cx).detach_and_log_err(cx); + } + }, + ) + .detach(); cx.emit(DapStoreEvent::DebugSessionInitialized(session_id)); })?; diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index c38dbe2f87..e6c459accc 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -832,7 +832,12 @@ pub enum SessionEvent { Threads, } +pub(crate) enum SessionStateEvent { + Shutdown, +} + impl EventEmitter for Session {} +impl EventEmitter for Session {} // local session will send breakpoint updates to DAP for all new breakpoints // remote side will only send breakpoint updates when it is a breakpoint created by that peer @@ -1553,6 +1558,8 @@ impl Session { ) }; + cx.emit(SessionStateEvent::Shutdown); + cx.background_spawn(async move { let _ = task.await; }) diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs index c981636670..923577fe5d 100644 --- a/crates/ui/src/components/dropdown_menu.rs +++ b/crates/ui/src/components/dropdown_menu.rs @@ -2,10 +2,15 @@ use gpui::{ClickEvent, Corner, CursorStyle, Entity, MouseButton}; use crate::{ContextMenu, PopoverMenu, prelude::*}; +enum LabelKind { + Text(SharedString), + Element(AnyElement), +} + #[derive(IntoElement)] pub struct DropdownMenu { id: ElementId, - label: SharedString, + label: LabelKind, menu: Entity, full_width: bool, disabled: bool, @@ -19,7 +24,21 @@ impl DropdownMenu { ) -> Self { Self { id: id.into(), - label: label.into(), + label: LabelKind::Text(label.into()), + menu, + full_width: false, + disabled: false, + } + } + + pub fn new_with_element( + id: impl Into, + label: AnyElement, + menu: Entity, + ) -> Self { + Self { + id: id.into(), + label: LabelKind::Element(label), menu, full_width: false, disabled: false, @@ -55,7 +74,7 @@ impl RenderOnce for DropdownMenu { #[derive(IntoElement)] struct DropdownMenuTrigger { - label: SharedString, + label: LabelKind, full_width: bool, selected: bool, disabled: bool, @@ -64,9 +83,9 @@ struct DropdownMenuTrigger { } impl DropdownMenuTrigger { - pub fn new(label: impl Into) -> Self { + pub fn new(label: LabelKind) -> Self { Self { - label: label.into(), + label, full_width: false, selected: false, disabled: false, @@ -135,11 +154,16 @@ impl RenderOnce for DropdownMenuTrigger { el.cursor_pointer() } }) - .child(Label::new(self.label).color(if disabled { - Color::Disabled - } else { - Color::Default - })) + .child(match self.label { + LabelKind::Text(text) => Label::new(text) + .color(if disabled { + Color::Disabled + } else { + Color::Default + }) + .into_any_element(), + LabelKind::Element(element) => element, + }) .child( Icon::new(IconName::ChevronUpDown) .size(IconSize::XSmall)