debugger: Improve appearance of session list for JavaScript debugging (#34322)

This PR updates the debugger panel's session list to be more useful in
some cases that are commonly hit when using the JavaScript adapter. We
make two adjustments, which only apply to JavaScript sessions:

- For a child session that's the only child of a root session, we
collapse it with its parent. This imitates what VS Code does in the
"call stack" view for JavaScript sessions.
- When a session has exactly one thread, we label the session with that
thread's name, instead of the session label provided by the DAP. VS Code
also makes this adjustment, which surfaces more useful information when
working with browser sessions.

Closes #33072 

Release Notes:

- debugger: Improved the appearance of JavaScript sessions in the debug
panel's session list.

---------

Co-authored-by: Julia <julia@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>
This commit is contained in:
Cole Miller 2025-07-12 11:56:05 -04:00 committed by GitHub
parent 13ddd5e4cb
commit a8cc927303
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 392 additions and 236 deletions

View file

@ -448,7 +448,7 @@ impl ActivityIndicator {
.into_any_element(), .into_any_element(),
), ),
message: format!("Debug: {}", session.read(cx).adapter()), message: format!("Debug: {}", session.read(cx).adapter()),
tooltip_message: Some(session.read(cx).label().to_string()), tooltip_message: session.read(cx).label().map(|label| label.to_string()),
on_click: None, on_click: None,
}); });
} }

View file

@ -378,6 +378,14 @@ pub trait DebugAdapter: 'static + Send + Sync {
fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> { fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
None None
} }
fn compact_child_session(&self) -> bool {
false
}
fn prefer_thread_name(&self) -> bool {
false
}
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]

View file

@ -534,6 +534,14 @@ impl DebugAdapter for JsDebugAdapter {
.filter(|name| !name.is_empty())?; .filter(|name| !name.is_empty())?;
Some(label.to_owned()) Some(label.to_owned())
} }
fn compact_child_session(&self) -> bool {
true
}
fn prefer_thread_name(&self) -> bool {
true
}
} }
fn normalize_task_type(task_type: &mut Value) { fn normalize_task_type(task_type: &mut Value) {

View file

@ -399,7 +399,8 @@ impl LogStore {
state.insert(DebugAdapterState::new( state.insert(DebugAdapterState::new(
id.session_id, id.session_id,
adapter_name, adapter_name,
session_label, session_label
.unwrap_or_else(|| format!("Session {} (child)", id.session_id.0).into()),
has_adapter_logs, has_adapter_logs,
)); ));

View file

@ -9,6 +9,7 @@ use crate::{
ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal, ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
}; };
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use collections::IndexMap;
use dap::adapters::DebugAdapterName; use dap::adapters::DebugAdapterName;
use dap::debugger_settings::DebugPanelDockPosition; use dap::debugger_settings::DebugPanelDockPosition;
use dap::{ use dap::{
@ -26,7 +27,7 @@ use text::ToPoint as _;
use itertools::Itertools as _; use itertools::Itertools as _;
use language::Buffer; use language::Buffer;
use project::debugger::session::{Session, SessionStateEvent}; use project::debugger::session::{Session, SessionQuirks, SessionStateEvent};
use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId}; use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId};
use project::{Project, debugger::session::ThreadStatus}; use project::{Project, debugger::session::ThreadStatus};
use rpc::proto::{self}; use rpc::proto::{self};
@ -63,13 +64,14 @@ pub enum DebugPanelEvent {
pub struct DebugPanel { pub struct DebugPanel {
size: Pixels, size: Pixels,
sessions: Vec<Entity<DebugSession>>,
active_session: Option<Entity<DebugSession>>, active_session: Option<Entity<DebugSession>>,
project: Entity<Project>, project: Entity<Project>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
debug_scenario_scheduled_last: bool, debug_scenario_scheduled_last: bool,
pub(crate) sessions_with_children:
IndexMap<Entity<DebugSession>, Vec<WeakEntity<DebugSession>>>,
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>, pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>, pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
@ -100,7 +102,7 @@ impl DebugPanel {
Self { Self {
size: px(300.), size: px(300.),
sessions: vec![], sessions_with_children: Default::default(),
active_session: None, active_session: None,
focus_handle, focus_handle,
breakpoint_list: BreakpointList::new( breakpoint_list: BreakpointList::new(
@ -138,8 +140,9 @@ impl DebugPanel {
}); });
} }
pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> { #[cfg(test)]
self.sessions.clone() pub(crate) fn sessions(&self) -> impl Iterator<Item = Entity<DebugSession>> {
self.sessions_with_children.keys().cloned()
} }
pub fn active_session(&self) -> Option<Entity<DebugSession>> { pub fn active_session(&self) -> Option<Entity<DebugSession>> {
@ -185,12 +188,20 @@ impl DebugPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let dap_store = self.project.read(cx).dap_store(); let dap_store = self.project.read(cx).dap_store();
let Some(adapter) = DapRegistry::global(cx).adapter(&scenario.adapter) else {
return;
};
let quirks = SessionQuirks {
compact: adapter.compact_child_session(),
prefer_thread_name: adapter.prefer_thread_name(),
};
let session = dap_store.update(cx, |dap_store, cx| { let session = dap_store.update(cx, |dap_store, cx| {
dap_store.new_session( dap_store.new_session(
scenario.label.clone(), Some(scenario.label.clone()),
DebugAdapterName(scenario.adapter.clone()), DebugAdapterName(scenario.adapter.clone()),
task_context.clone(), task_context.clone(),
None, None,
quirks,
cx, cx,
) )
}); });
@ -363,14 +374,15 @@ impl DebugPanel {
}; };
let dap_store_handle = self.project.read(cx).dap_store().clone(); let dap_store_handle = self.project.read(cx).dap_store().clone();
let label = curr_session.read(cx).label().clone(); let label = curr_session.read(cx).label();
let quirks = curr_session.read(cx).quirks();
let adapter = curr_session.read(cx).adapter().clone(); let adapter = curr_session.read(cx).adapter().clone();
let binary = curr_session.read(cx).binary().cloned().unwrap(); let binary = curr_session.read(cx).binary().cloned().unwrap();
let task_context = curr_session.read(cx).task_context().clone(); let task_context = curr_session.read(cx).task_context().clone();
let curr_session_id = curr_session.read(cx).session_id(); let curr_session_id = curr_session.read(cx).session_id();
self.sessions self.sessions_with_children
.retain(|session| session.read(cx).session_id(cx) != curr_session_id); .retain(|session, _| session.read(cx).session_id(cx) != curr_session_id);
let task = dap_store_handle.update(cx, |dap_store, cx| { let task = dap_store_handle.update(cx, |dap_store, cx| {
dap_store.shutdown_session(curr_session_id, cx) dap_store.shutdown_session(curr_session_id, cx)
}); });
@ -379,7 +391,7 @@ impl DebugPanel {
task.await.log_err(); task.await.log_err();
let (session, task) = dap_store_handle.update(cx, |dap_store, cx| { let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
let session = dap_store.new_session(label, adapter, task_context, None, cx); let session = dap_store.new_session(label, adapter, task_context, None, quirks, cx);
let task = session.update(cx, |session, cx| { let task = session.update(cx, |session, cx| {
session.boot(binary, worktree, dap_store_handle.downgrade(), cx) session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
@ -425,6 +437,7 @@ impl DebugPanel {
let dap_store_handle = self.project.read(cx).dap_store().clone(); let dap_store_handle = self.project.read(cx).dap_store().clone();
let label = self.label_for_child_session(&parent_session, request, cx); let label = self.label_for_child_session(&parent_session, request, cx);
let adapter = parent_session.read(cx).adapter().clone(); let adapter = parent_session.read(cx).adapter().clone();
let quirks = parent_session.read(cx).quirks();
let Some(mut binary) = parent_session.read(cx).binary().cloned() else { let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
log::error!("Attempted to start a child-session without a binary"); log::error!("Attempted to start a child-session without a binary");
return; return;
@ -438,6 +451,7 @@ impl DebugPanel {
adapter, adapter,
task_context, task_context,
Some(parent_session.clone()), Some(parent_session.clone()),
quirks,
cx, cx,
); );
@ -463,8 +477,8 @@ impl DebugPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let Some(session) = self let Some(session) = self
.sessions .sessions_with_children
.iter() .keys()
.find(|other| entity_id == other.entity_id()) .find(|other| entity_id == other.entity_id())
.cloned() .cloned()
else { else {
@ -498,15 +512,14 @@ impl DebugPanel {
} }
session.update(cx, |session, cx| session.shutdown(cx)).ok(); session.update(cx, |session, cx| session.shutdown(cx)).ok();
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.sessions.retain(|other| entity_id != other.entity_id()); this.retain_sessions(|other| entity_id != other.entity_id());
if let Some(active_session_id) = this if let Some(active_session_id) = this
.active_session .active_session
.as_ref() .as_ref()
.map(|session| session.entity_id()) .map(|session| session.entity_id())
{ {
if active_session_id == entity_id { if active_session_id == entity_id {
this.active_session = this.sessions.first().cloned(); this.active_session = this.sessions_with_children.keys().next().cloned();
} }
} }
cx.notify() cx.notify()
@ -976,8 +989,8 @@ impl DebugPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if let Some(session) = self if let Some(session) = self
.sessions .sessions_with_children
.iter() .keys()
.find(|session| session.read(cx).session_id(cx) == session_id) .find(|session| session.read(cx).session_id(cx) == session_id)
{ {
self.activate_session(session.clone(), window, cx); self.activate_session(session.clone(), window, cx);
@ -990,7 +1003,7 @@ impl DebugPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
debug_assert!(self.sessions.contains(&session_item)); debug_assert!(self.sessions_with_children.contains_key(&session_item));
session_item.focus_handle(cx).focus(window); session_item.focus_handle(cx).focus(window);
session_item.update(cx, |this, cx| { session_item.update(cx, |this, cx| {
this.running_state().update(cx, |this, cx| { this.running_state().update(cx, |this, cx| {
@ -1261,18 +1274,27 @@ impl DebugPanel {
parent_session: &Entity<Session>, parent_session: &Entity<Session>,
request: &StartDebuggingRequestArguments, request: &StartDebuggingRequestArguments,
cx: &mut Context<'_, Self>, cx: &mut Context<'_, Self>,
) -> SharedString { ) -> Option<SharedString> {
let adapter = parent_session.read(cx).adapter(); let adapter = parent_session.read(cx).adapter();
if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) { if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
if let Some(label) = adapter.label_for_child_session(request) { if let Some(label) = adapter.label_for_child_session(request) {
return label.into(); return Some(label.into());
} }
} }
let mut label = parent_session.read(cx).label().clone(); None
if !label.ends_with("(child)") { }
label = format!("{label} (child)").into();
fn retain_sessions(&mut self, keep: impl Fn(&Entity<DebugSession>) -> bool) {
self.sessions_with_children
.retain(|session, _| keep(session));
for children in self.sessions_with_children.values_mut() {
children.retain(|child| {
let Some(child) = child.upgrade() else {
return false;
};
keep(&child)
});
} }
label
} }
} }
@ -1302,11 +1324,11 @@ async fn register_session_inner(
let serialized_layout = persistence::get_serialized_layout(adapter_name).await; let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
let debug_session = this.update_in(cx, |this, window, cx| { let debug_session = this.update_in(cx, |this, window, cx| {
let parent_session = this let parent_session = this
.sessions .sessions_with_children
.iter() .keys()
.find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx)) .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
.cloned(); .cloned();
this.sessions.retain(|session| { this.retain_sessions(|session| {
!session !session
.read(cx) .read(cx)
.running_state() .running_state()
@ -1337,13 +1359,23 @@ async fn register_session_inner(
) )
.detach(); .detach();
let insert_position = this let insert_position = this
.sessions .sessions_with_children
.iter() .keys()
.position(|session| Some(session) == parent_session.as_ref()) .position(|session| Some(session) == parent_session.as_ref())
.map(|position| position + 1) .map(|position| position + 1)
.unwrap_or(this.sessions.len()); .unwrap_or(this.sessions_with_children.len());
// Maintain topological sort order of sessions // Maintain topological sort order of sessions
this.sessions.insert(insert_position, debug_session.clone()); let (_, old) = this.sessions_with_children.insert_before(
insert_position,
debug_session.clone(),
Default::default(),
);
debug_assert!(old.is_none());
if let Some(parent_session) = parent_session {
this.sessions_with_children
.entry(parent_session)
.and_modify(|children| children.push(debug_session.downgrade()));
}
debug_session debug_session
})?; })?;
@ -1383,7 +1415,7 @@ impl Panel for DebugPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
if position.axis() != self.position(window, cx).axis() { if position.axis() != self.position(window, cx).axis() {
self.sessions.iter().for_each(|session_item| { self.sessions_with_children.keys().for_each(|session_item| {
session_item.update(cx, |item, cx| { session_item.update(cx, |item, cx| {
item.running_state() item.running_state()
.update(cx, |state, _| state.invert_axies()) .update(cx, |state, _| state.invert_axies())

View file

@ -1,16 +1,82 @@
use std::time::Duration; use std::{rc::Rc, time::Duration};
use collections::HashMap; use collections::HashMap;
use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage}; use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
use project::debugger::session::{ThreadId, ThreadStatus}; use project::debugger::session::{ThreadId, ThreadStatus};
use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
use util::truncate_and_trailoff; use util::{maybe, truncate_and_trailoff};
use crate::{ use crate::{
debugger_panel::DebugPanel, debugger_panel::DebugPanel,
session::{DebugSession, running::RunningState}, session::{DebugSession, running::RunningState},
}; };
struct SessionListEntry {
ancestors: Vec<Entity<DebugSession>>,
leaf: Entity<DebugSession>,
}
impl SessionListEntry {
pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement {
const MAX_LABEL_CHARS: usize = 150;
let mut label = String::new();
for ancestor in &self.ancestors {
label.push_str(&ancestor.update(cx, |ancestor, cx| {
ancestor.label(cx).unwrap_or("(child)".into())
}));
label.push_str(" » ");
}
label.push_str(
&self
.leaf
.update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())),
);
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
let is_terminated = self
.leaf
.read(cx)
.running_state
.read(cx)
.session()
.read(cx)
.is_terminated();
let icon = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match self
.leaf
.read(cx)
.running_state
.read(cx)
.thread_status(cx)
.unwrap_or_default()
{
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
}
}
};
h_flex()
.id("session-label")
.ml(depth * px(16.0))
.gap_2()
.when_some(icon, |this, indicator| this.child(indicator))
.justify_between()
.child(
Label::new(label)
.size(LabelSize::Small)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element()
}
}
impl DebugPanel { impl DebugPanel {
fn dropdown_label(label: impl Into<SharedString>) -> Label { fn dropdown_label(label: impl Into<SharedString>) -> Label {
const MAX_LABEL_CHARS: usize = 50; const MAX_LABEL_CHARS: usize = 50;
@ -25,145 +91,205 @@ impl DebugPanel {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Option<impl IntoElement> { ) -> Option<impl IntoElement> {
if let Some(running_state) = running_state { let running_state = running_state?;
let sessions = self.sessions().clone();
let weak = cx.weak_entity();
let running_state = running_state.read(cx);
let label = if let Some(active_session) = active_session.clone() {
active_session.read(cx).session(cx).read(cx).label()
} else {
SharedString::new_static("Unknown Session")
};
let is_terminated = running_state.session().read(cx).is_terminated(); let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
let is_started = active_session let mut sessions_with_children = self.sessions_with_children.iter().peekable();
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
let session_state_indicator = if is_terminated { while let Some((root, children)) = sessions_with_children.next() {
Indicator::dot().color(Color::Error).into_any_element() let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice())
} else if !is_started { && let Some(single_child) = single_child.upgrade()
Icon::new(IconName::ArrowCircle) && single_child.read(cx).quirks.compact
.size(IconSize::Small) {
.color(Color::Muted) sessions_with_children.next();
.with_animation( SessionListEntry {
"arrow-circle", leaf: single_child.clone(),
Animation::new(Duration::from_secs(2)).repeat(), ancestors: vec![root.clone()],
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))), }
)
.into_any_element()
} else { } else {
match running_state.thread_status(cx).unwrap_or_default() { SessionListEntry {
ThreadStatus::Stopped => { leaf: root.clone(),
Indicator::dot().color(Color::Conflict).into_any_element() ancestors: Vec::new(),
}
_ => Indicator::dot().color(Color::Success).into_any_element(),
} }
}; };
session_entries.push(root_entry);
let trigger = h_flex() session_entries.extend(
.gap_2() sessions_with_children
.child(session_state_indicator) .by_ref()
.justify_between() .take_while(|(session, _)| {
.child( session
DebugPanel::dropdown_label(label) .read(cx)
.when(is_terminated, |this| this.strikethrough()), .session(cx)
) .read(cx)
.into_any_element(); .parent_id(cx)
.is_some()
Some( })
DropdownMenu::new_with_element( .map(|(session, _)| SessionListEntry {
"debugger-session-list", leaf: session.clone(),
trigger, ancestors: vec![],
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
let mut session_depths = HashMap::default();
for session in sessions.into_iter() {
let weak_session = session.downgrade();
let weak_session_id = weak_session.entity_id();
let session_id = session.read(cx).session_id(cx);
let parent_depth = session
.read(cx)
.session(cx)
.read(cx)
.parent_id(cx)
.and_then(|parent_id| session_depths.get(&parent_id).cloned());
let self_depth =
*session_depths.entry(session_id).or_insert_with(|| {
parent_depth.map(|depth| depth + 1).unwrap_or(0usize)
});
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
move |_, cx| {
weak_session
.read_with(cx, |session, cx| {
let context_menu = context_menu.clone();
let id: SharedString =
format!("debug-session-{}", session_id.0)
.into();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session.label_element(self_depth, cx))
.child(
IconButton::new(
"close-debug-session",
IconName::Close,
)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(
weak_session_id,
window,
cx,
);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(
&Default::default(),
window,
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
}), }),
) );
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone()),
)
} else {
None
} }
let weak = cx.weak_entity();
let trigger_label = if let Some(active_session) = active_session.clone() {
active_session.update(cx, |active_session, cx| {
active_session.label(cx).unwrap_or("(child)".into())
})
} else {
SharedString::new_static("Unknown Session")
};
let running_state = running_state.read(cx);
let is_terminated = running_state.session().read(cx).is_terminated();
let is_started = active_session
.is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
let session_state_indicator = if is_terminated {
Indicator::dot().color(Color::Error).into_any_element()
} else if !is_started {
Icon::new(IconName::ArrowCircle)
.size(IconSize::Small)
.color(Color::Muted)
.with_animation(
"arrow-circle",
Animation::new(Duration::from_secs(2)).repeat(),
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
)
.into_any_element()
} else {
match running_state.thread_status(cx).unwrap_or_default() {
ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
_ => Indicator::dot().color(Color::Success).into_any_element(),
}
};
let trigger = h_flex()
.gap_2()
.child(session_state_indicator)
.justify_between()
.child(
DebugPanel::dropdown_label(trigger_label)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element();
let menu = DropdownMenu::new_with_element(
"debugger-session-list",
trigger,
ContextMenu::build(window, cx, move |mut this, _, cx| {
let context_menu = cx.weak_entity();
let mut session_depths = HashMap::default();
for session_entry in session_entries {
let session_id = session_entry.leaf.read(cx).session_id(cx);
let parent_depth = session_entry
.ancestors
.first()
.unwrap_or(&session_entry.leaf)
.read(cx)
.session(cx)
.read(cx)
.parent_id(cx)
.and_then(|parent_id| session_depths.get(&parent_id).cloned());
let self_depth = *session_depths
.entry(session_id)
.or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
this = this.custom_entry(
{
let weak = weak.clone();
let context_menu = context_menu.clone();
let ancestors: Rc<[_]> = session_entry
.ancestors
.iter()
.map(|session| session.downgrade())
.collect();
let leaf = session_entry.leaf.downgrade();
move |window, cx| {
Self::render_session_menu_entry(
weak.clone(),
context_menu.clone(),
ancestors.clone(),
leaf.clone(),
self_depth,
window,
cx,
)
}
},
{
let weak = weak.clone();
let leaf = session_entry.leaf.clone();
move |window, cx| {
weak.update(cx, |panel, cx| {
panel.activate_session(leaf.clone(), window, cx);
})
.ok();
}
},
);
}
this
}),
)
.style(DropdownStyle::Ghost)
.handle(self.session_picker_menu_handle.clone());
Some(menu)
}
fn render_session_menu_entry(
weak: WeakEntity<DebugPanel>,
context_menu: WeakEntity<ContextMenu>,
ancestors: Rc<[WeakEntity<DebugSession>]>,
leaf: WeakEntity<DebugSession>,
self_depth: usize,
_window: &mut Window,
cx: &mut App,
) -> AnyElement {
let Some(session_entry) = maybe!({
let ancestors = ancestors
.iter()
.map(|ancestor| ancestor.upgrade())
.collect::<Option<Vec<_>>>()?;
let leaf = leaf.upgrade()?;
Some(SessionListEntry { ancestors, leaf })
}) else {
return div().into_any_element();
};
let id: SharedString = format!(
"debug-session-{}",
session_entry.leaf.read(cx).session_id(cx).0
)
.into();
let session_entity_id = session_entry.leaf.entity_id();
h_flex()
.w_full()
.group(id.clone())
.justify_between()
.child(session_entry.label_element(self_depth, cx))
.child(
IconButton::new("close-debug-session", IconName::Close)
.visible_on_hover(id.clone())
.icon_size(IconSize::Small)
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |panel, cx| {
panel.close_session(session_entity_id, window, cx);
})
.ok();
context_menu
.update(cx, |this, cx| {
this.cancel(&Default::default(), window, cx);
})
.ok();
}
}),
)
.into_any_element()
} }
pub(crate) fn render_thread_dropdown( pub(crate) fn render_thread_dropdown(

View file

@ -5,14 +5,13 @@ use dap::client::SessionId;
use gpui::{ use gpui::{
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
}; };
use project::Project;
use project::debugger::session::Session; use project::debugger::session::Session;
use project::worktree_store::WorktreeStore; use project::worktree_store::WorktreeStore;
use project::{Project, debugger::session::SessionQuirks};
use rpc::proto; use rpc::proto;
use running::RunningState; use running::RunningState;
use std::{cell::OnceCell, sync::OnceLock}; use std::cell::OnceCell;
use ui::{Indicator, prelude::*}; use ui::prelude::*;
use util::truncate_and_trailoff;
use workspace::{ use workspace::{
CollaboratorId, FollowableItem, ViewId, Workspace, CollaboratorId, FollowableItem, ViewId, Workspace,
item::{self, Item}, item::{self, Item},
@ -20,8 +19,8 @@ use workspace::{
pub struct DebugSession { pub struct DebugSession {
remote_id: Option<workspace::ViewId>, remote_id: Option<workspace::ViewId>,
running_state: Entity<RunningState>, pub(crate) running_state: Entity<RunningState>,
label: OnceLock<SharedString>, pub(crate) quirks: SessionQuirks,
stack_trace_view: OnceCell<Entity<StackTraceView>>, stack_trace_view: OnceCell<Entity<StackTraceView>>,
_worktree_store: WeakEntity<WorktreeStore>, _worktree_store: WeakEntity<WorktreeStore>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
@ -57,6 +56,7 @@ impl DebugSession {
cx, cx,
) )
}); });
let quirks = session.read(cx).quirks();
cx.new(|cx| Self { cx.new(|cx| Self {
_subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| { _subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
@ -64,7 +64,7 @@ impl DebugSession {
})], })],
remote_id: None, remote_id: None,
running_state, running_state,
label: OnceLock::new(), quirks,
stack_trace_view: OnceCell::new(), stack_trace_view: OnceCell::new(),
_worktree_store: project.read(cx).worktree_store().downgrade(), _worktree_store: project.read(cx).worktree_store().downgrade(),
workspace, workspace,
@ -110,65 +110,29 @@ impl DebugSession {
.update(cx, |state, cx| state.shutdown(cx)); .update(cx, |state, cx| state.shutdown(cx));
} }
pub(crate) fn label(&self, cx: &App) -> SharedString { pub(crate) fn label(&self, cx: &mut App) -> Option<SharedString> {
if let Some(label) = self.label.get() { let session = self.running_state.read(cx).session().clone();
return label.clone(); session.update(cx, |session, cx| {
} let session_label = session.label();
let quirks = session.quirks();
let session = self.running_state.read(cx).session(); let mut single_thread_name = || {
let threads = session.threads(cx);
self.label match threads.as_slice() {
.get_or_init(|| session.read(cx).label()) [(thread, _)] => Some(SharedString::from(&thread.name)),
.to_owned() _ => None,
}
};
if quirks.prefer_thread_name {
single_thread_name().or(session_label)
} else {
session_label.or_else(single_thread_name)
}
})
} }
pub fn running_state(&self) -> &Entity<RunningState> { pub fn running_state(&self) -> &Entity<RunningState> {
&self.running_state &self.running_state
} }
pub(crate) fn label_element(&self, depth: usize, cx: &App) -> AnyElement {
const MAX_LABEL_CHARS: usize = 150;
let label = self.label(cx);
let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
let is_terminated = self
.running_state
.read(cx)
.session()
.read(cx)
.is_terminated();
let icon = {
if is_terminated {
Some(Indicator::dot().color(Color::Error))
} else {
match self
.running_state
.read(cx)
.thread_status(cx)
.unwrap_or_default()
{
project::debugger::session::ThreadStatus::Stopped => {
Some(Indicator::dot().color(Color::Conflict))
}
_ => Some(Indicator::dot().color(Color::Success)),
}
}
};
h_flex()
.id("session-label")
.ml(depth * px(16.0))
.gap_2()
.when_some(icon, |this, indicator| this.child(indicator))
.justify_between()
.child(
Label::new(label)
.size(LabelSize::Small)
.when(is_terminated, |this| this.strikethrough()),
)
.into_any_element()
}
} }
impl EventEmitter<DebugPanelItemEvent> for DebugSession {} impl EventEmitter<DebugPanelItemEvent> for DebugSession {}

View file

@ -427,7 +427,7 @@ async fn test_handle_start_debugging_request(
let sessions = workspace let sessions = workspace
.update(cx, |workspace, _window, cx| { .update(cx, |workspace, _window, cx| {
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
debug_panel.read(cx).sessions() debug_panel.read(cx).sessions().collect::<Vec<_>>()
}) })
.unwrap(); .unwrap();
assert_eq!(sessions.len(), 1); assert_eq!(sessions.len(), 1);
@ -451,7 +451,7 @@ async fn test_handle_start_debugging_request(
.unwrap() .unwrap()
.read(cx) .read(cx)
.session(cx); .session(cx);
let current_sessions = debug_panel.read(cx).sessions(); let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>();
assert_eq!(active_session, current_sessions[1].read(cx).session(cx)); assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
assert_eq!( assert_eq!(
active_session.read(cx).parent_session(), active_session.read(cx).parent_session(),
@ -1796,7 +1796,7 @@ async fn test_debug_adapters_shutdown_on_app_quit(
let panel = workspace.panel::<DebugPanel>(cx).unwrap(); let panel = workspace.panel::<DebugPanel>(cx).unwrap();
panel.read_with(cx, |panel, _| { panel.read_with(cx, |panel, _| {
assert!( assert!(
!panel.sessions().is_empty(), panel.sessions().next().is_some(),
"Debug session should be active" "Debug session should be active"
); );
}); });

View file

@ -6,6 +6,7 @@ use super::{
}; };
use crate::{ use crate::{
InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState,
debugger::session::SessionQuirks,
project_settings::ProjectSettings, project_settings::ProjectSettings,
terminals::{SshCommand, wrap_for_ssh}, terminals::{SshCommand, wrap_for_ssh},
worktree_store::WorktreeStore, worktree_store::WorktreeStore,
@ -385,10 +386,11 @@ impl DapStore {
pub fn new_session( pub fn new_session(
&mut self, &mut self,
label: SharedString, label: Option<SharedString>,
adapter: DebugAdapterName, adapter: DebugAdapterName,
task_context: TaskContext, task_context: TaskContext,
parent_session: Option<Entity<Session>>, parent_session: Option<Entity<Session>>,
quirks: SessionQuirks,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Entity<Session> { ) -> Entity<Session> {
let session_id = SessionId(util::post_inc(&mut self.next_session_id)); let session_id = SessionId(util::post_inc(&mut self.next_session_id));
@ -406,6 +408,7 @@ impl DapStore {
label, label,
adapter, adapter,
task_context, task_context,
quirks,
cx, cx,
); );

View file

@ -151,6 +151,12 @@ pub struct RunningMode {
messages_tx: UnboundedSender<Message>, messages_tx: UnboundedSender<Message>,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct SessionQuirks {
pub compact: bool,
pub prefer_thread_name: bool,
}
fn client_source(abs_path: &Path) -> dap::Source { fn client_source(abs_path: &Path) -> dap::Source {
dap::Source { dap::Source {
name: abs_path name: abs_path
@ -656,7 +662,7 @@ pub struct OutputToken(pub usize);
pub struct Session { pub struct Session {
pub mode: Mode, pub mode: Mode,
id: SessionId, id: SessionId,
label: SharedString, label: Option<SharedString>,
adapter: DebugAdapterName, adapter: DebugAdapterName,
pub(super) capabilities: Capabilities, pub(super) capabilities: Capabilities,
child_session_ids: HashSet<SessionId>, child_session_ids: HashSet<SessionId>,
@ -679,6 +685,7 @@ pub struct Session {
background_tasks: Vec<Task<()>>, background_tasks: Vec<Task<()>>,
restart_task: Option<Task<()>>, restart_task: Option<Task<()>>,
task_context: TaskContext, task_context: TaskContext,
quirks: SessionQuirks,
} }
trait CacheableCommand: Any + Send + Sync { trait CacheableCommand: Any + Send + Sync {
@ -792,9 +799,10 @@ impl Session {
breakpoint_store: Entity<BreakpointStore>, breakpoint_store: Entity<BreakpointStore>,
session_id: SessionId, session_id: SessionId,
parent_session: Option<Entity<Session>>, parent_session: Option<Entity<Session>>,
label: SharedString, label: Option<SharedString>,
adapter: DebugAdapterName, adapter: DebugAdapterName,
task_context: TaskContext, task_context: TaskContext,
quirks: SessionQuirks,
cx: &mut App, cx: &mut App,
) -> Entity<Self> { ) -> Entity<Self> {
cx.new::<Self>(|cx| { cx.new::<Self>(|cx| {
@ -848,6 +856,7 @@ impl Session {
label, label,
adapter, adapter,
task_context, task_context,
quirks,
}; };
this this
@ -1022,7 +1031,7 @@ impl Session {
self.adapter.clone() self.adapter.clone()
} }
pub fn label(&self) -> SharedString { pub fn label(&self) -> Option<SharedString> {
self.label.clone() self.label.clone()
} }
@ -2481,4 +2490,8 @@ impl Session {
pub fn thread_state(&self, thread_id: ThreadId) -> Option<ThreadStatus> { pub fn thread_state(&self, thread_id: ThreadId) -> Option<ThreadStatus> {
self.thread_states.thread_state(thread_id) self.thread_states.thread_state(thread_id)
} }
pub fn quirks(&self) -> SessionQuirks {
self.quirks
}
} }

View file

@ -4301,6 +4301,7 @@ impl ProjectPanel {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let components_len = components.len(); let components_len = components.len();
// TODO this can underflow
let active_index = components_len let active_index = components_len
- 1 - 1
- folded_ancestors.current_ancestor_depth; - folded_ancestors.current_ancestor_depth;