debugger: Add close button and coloring to debug panel session's menu (#28310)

This PR adds colors to debug panel's session menu that indicate the
state of each respective session. It also adds a close button to each
entry.

green - running
yellow - stopped
red - terminated/ended 


Release Notes:

- N/A
This commit is contained in:
Anthony Eid 2025-04-08 12:35:33 -04:00 committed by GitHub
parent ee7b1ec7f2
commit 1774cad933
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 173 additions and 44 deletions

View file

@ -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)

View file

@ -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 {}

View file

@ -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.

View file

@ -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));
})?;

View file

@ -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;
})

View file

@ -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)