Detailed changes
@@ -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<Self>) {
+ 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<DebugSession>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> 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<Self>) -> Option<Div> {
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)
@@ -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<Self>) {
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<DebugPanelItemEvent> for DebugSession {}
@@ -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.
@@ -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));
})?;
@@ -832,7 +832,12 @@ pub enum SessionEvent {
Threads,
}
+pub(crate) enum SessionStateEvent {
+ Shutdown,
+}
+
impl EventEmitter<SessionEvent> for Session {}
+impl EventEmitter<SessionStateEvent> 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;
})
@@ -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<ContextMenu>,
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<ElementId>,
+ label: AnyElement,
+ menu: Entity<ContextMenu>,
+ ) -> 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<SharedString>) -> 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)