From a8cc92730363691c624f3a4671654c1e17f4f9ed Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Sat, 12 Jul 2025 11:56:05 -0400 Subject: [PATCH] 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 Co-authored-by: Remco Smits --- .../src/activity_indicator.rs | 2 +- crates/dap/src/adapters.rs | 8 + crates/dap_adapters/src/javascript.rs | 8 + crates/debugger_tools/src/dap_log.rs | 3 +- crates/debugger_ui/src/debugger_panel.rs | 96 +++-- crates/debugger_ui/src/dropdown_menus.rs | 390 ++++++++++++------ crates/debugger_ui/src/session.rs | 86 ++-- .../debugger_ui/src/tests/debugger_panel.rs | 6 +- crates/project/src/debugger/dap_store.rs | 5 +- crates/project/src/debugger/session.rs | 19 +- crates/project_panel/src/project_panel.rs | 1 + 11 files changed, 390 insertions(+), 234 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index b07c5418218c5045ae8018c9be9ec6fd07446544..aee25fc9e39d533409b980782fa8f0cac3977935 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -448,7 +448,7 @@ impl ActivityIndicator { .into_any_element(), ), 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, }); } diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index bd36b073872c38b20af0a6b6a95f06e0374fe2d0..0c88f37ff8bfaad92ef5d6223b43c9bd6d91ad1d 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -378,6 +378,14 @@ pub trait DebugAdapter: 'static + Send + Sync { fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option { None } + + fn compact_child_session(&self) -> bool { + false + } + + fn prefer_thread_name(&self) -> bool { + false + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 76c1d1fb7bb3b2b3a534293957b43919a079a888..a51377cd76dd7ab1702c263378d0bf2904f27a6f 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -534,6 +534,14 @@ impl DebugAdapter for JsDebugAdapter { .filter(|name| !name.is_empty())?; 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) { diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index f53b6403b235ec43a141c67b6f7108316df18adb..b806381d251c6595a5dd12022dc3d1df8b71739f 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -399,7 +399,8 @@ impl LogStore { state.insert(DebugAdapterState::new( id.session_id, adapter_name, - session_label, + session_label + .unwrap_or_else(|| format!("Session {} (child)", id.session_id.0).into()), has_adapter_logs, )); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c90a2878e925ceb7d44a80c87bc8d9a7945b4fa0..184aedafc2dd10752158350e53f673a4d0122dbb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -9,6 +9,7 @@ use crate::{ ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal, }; use anyhow::{Context as _, Result, anyhow}; +use collections::IndexMap; use dap::adapters::DebugAdapterName; use dap::debugger_settings::DebugPanelDockPosition; use dap::{ @@ -26,7 +27,7 @@ use text::ToPoint as _; use itertools::Itertools as _; 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::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; @@ -63,13 +64,14 @@ pub enum DebugPanelEvent { pub struct DebugPanel { size: Pixels, - sessions: Vec>, active_session: Option>, project: Entity, workspace: WeakEntity, focus_handle: FocusHandle, context_menu: Option<(Entity, Point, Subscription)>, debug_scenario_scheduled_last: bool, + pub(crate) sessions_with_children: + IndexMap, Vec>>, pub(crate) thread_picker_menu_handle: PopoverMenuHandle, pub(crate) session_picker_menu_handle: PopoverMenuHandle, fs: Arc, @@ -100,7 +102,7 @@ impl DebugPanel { Self { size: px(300.), - sessions: vec![], + sessions_with_children: Default::default(), active_session: None, focus_handle, breakpoint_list: BreakpointList::new( @@ -138,8 +140,9 @@ impl DebugPanel { }); } - pub(crate) fn sessions(&self) -> Vec> { - self.sessions.clone() + #[cfg(test)] + pub(crate) fn sessions(&self) -> impl Iterator> { + self.sessions_with_children.keys().cloned() } pub fn active_session(&self) -> Option> { @@ -185,12 +188,20 @@ impl DebugPanel { cx: &mut Context, ) { 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| { dap_store.new_session( - scenario.label.clone(), + Some(scenario.label.clone()), DebugAdapterName(scenario.adapter.clone()), task_context.clone(), None, + quirks, cx, ) }); @@ -363,14 +374,15 @@ impl DebugPanel { }; 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 binary = curr_session.read(cx).binary().cloned().unwrap(); let task_context = curr_session.read(cx).task_context().clone(); let curr_session_id = curr_session.read(cx).session_id(); - self.sessions - .retain(|session| session.read(cx).session_id(cx) != curr_session_id); + self.sessions_with_children + .retain(|session, _| session.read(cx).session_id(cx) != curr_session_id); let task = dap_store_handle.update(cx, |dap_store, cx| { dap_store.shutdown_session(curr_session_id, cx) }); @@ -379,7 +391,7 @@ impl DebugPanel { task.await.log_err(); 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| { 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 label = self.label_for_child_session(&parent_session, request, cx); 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 { log::error!("Attempted to start a child-session without a binary"); return; @@ -438,6 +451,7 @@ impl DebugPanel { adapter, task_context, Some(parent_session.clone()), + quirks, cx, ); @@ -463,8 +477,8 @@ impl DebugPanel { cx: &mut Context, ) { let Some(session) = self - .sessions - .iter() + .sessions_with_children + .keys() .find(|other| entity_id == other.entity_id()) .cloned() else { @@ -498,15 +512,14 @@ impl DebugPanel { } session.update(cx, |session, cx| session.shutdown(cx)).ok(); 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 .active_session .as_ref() .map(|session| session.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() @@ -976,8 +989,8 @@ impl DebugPanel { cx: &mut Context, ) { if let Some(session) = self - .sessions - .iter() + .sessions_with_children + .keys() .find(|session| session.read(cx).session_id(cx) == session_id) { self.activate_session(session.clone(), window, cx); @@ -990,7 +1003,7 @@ impl DebugPanel { window: &mut Window, cx: &mut Context, ) { - 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.update(cx, |this, cx| { this.running_state().update(cx, |this, cx| { @@ -1261,18 +1274,27 @@ impl DebugPanel { parent_session: &Entity, request: &StartDebuggingRequestArguments, cx: &mut Context<'_, Self>, - ) -> SharedString { + ) -> Option { let adapter = parent_session.read(cx).adapter(); if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) { 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(); - if !label.ends_with("(child)") { - label = format!("{label} (child)").into(); + None + } + + fn retain_sessions(&mut self, keep: impl Fn(&Entity) -> 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 debug_session = this.update_in(cx, |this, window, cx| { let parent_session = this - .sessions - .iter() + .sessions_with_children + .keys() .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx)) .cloned(); - this.sessions.retain(|session| { + this.retain_sessions(|session| { !session .read(cx) .running_state() @@ -1337,13 +1359,23 @@ async fn register_session_inner( ) .detach(); let insert_position = this - .sessions - .iter() + .sessions_with_children + .keys() .position(|session| Some(session) == parent_session.as_ref()) .map(|position| position + 1) - .unwrap_or(this.sessions.len()); + .unwrap_or(this.sessions_with_children.len()); // 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 })?; @@ -1383,7 +1415,7 @@ impl Panel for DebugPanel { cx: &mut Context, ) { 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| { item.running_state() .update(cx, |state, _| state.invert_axies()) diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs index f93aceae094db9a75b9550021c97bb9723ad6811..dca15eb0527cfc78bd137889a1910e6b32abf98c 100644 --- a/crates/debugger_ui/src/dropdown_menus.rs +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -1,16 +1,82 @@ -use std::time::Duration; +use std::{rc::Rc, time::Duration}; 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 ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; -use util::truncate_and_trailoff; +use util::{maybe, truncate_and_trailoff}; use crate::{ debugger_panel::DebugPanel, session::{DebugSession, running::RunningState}, }; +struct SessionListEntry { + ancestors: Vec>, + leaf: Entity, +} + +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 { fn dropdown_label(label: impl Into) -> Label { const MAX_LABEL_CHARS: usize = 50; @@ -25,145 +91,205 @@ impl DebugPanel { window: &mut Window, cx: &mut Context, ) -> Option { - if let Some(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 running_state = running_state?; + + let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3); + let mut sessions_with_children = self.sessions_with_children.iter().peekable(); - 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() + while let Some((root, children)) = sessions_with_children.next() { + let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice()) + && let Some(single_child) = single_child.upgrade() + && single_child.read(cx).quirks.compact + { + sessions_with_children.next(); + SessionListEntry { + leaf: single_child.clone(), + ancestors: vec![root.clone()], + } } 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(), + SessionListEntry { + leaf: root.clone(), + ancestors: Vec::new(), } }; + session_entries.push(root_entry); + + session_entries.extend( + sessions_with_children + .by_ref() + .take_while(|(session, _)| { + session + .read(cx) + .session(cx) + .read(cx) + .parent_id(cx) + .is_some() + }) + .map(|(session, _)| SessionListEntry { + leaf: session.clone(), + ancestors: vec![], + }), + ); + } - let trigger = h_flex() - .gap_2() - .child(session_state_indicator) - .justify_between() - .child( - DebugPanel::dropdown_label(label) - .when(is_terminated, |this| this.strikethrough()), + 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(); + .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(), + } + }; - Some( - 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 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(); - } - }, - ); + 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, + context_menu: WeakEntity, + ancestors: Rc<[WeakEntity]>, + leaf: WeakEntity, + self_depth: usize, + _window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let Some(session_entry) = maybe!({ + let ancestors = ancestors + .iter() + .map(|ancestor| ancestor.upgrade()) + .collect::>>()?; + 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(); } - this }), - ) - .style(DropdownStyle::Ghost) - .handle(self.session_picker_menu_handle.clone()), ) - } else { - None - } + .into_any_element() } pub(crate) fn render_thread_dropdown( diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 3c4c830b46cc27faa5f6dc6a4243041d094fe2e1..73cfef78cc6410196441ff974f09b5abe3d86916 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -5,14 +5,13 @@ use dap::client::SessionId; use gpui::{ App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::Project; use project::debugger::session::Session; use project::worktree_store::WorktreeStore; +use project::{Project, debugger::session::SessionQuirks}; use rpc::proto; use running::RunningState; -use std::{cell::OnceCell, sync::OnceLock}; -use ui::{Indicator, prelude::*}; -use util::truncate_and_trailoff; +use std::cell::OnceCell; +use ui::prelude::*; use workspace::{ CollaboratorId, FollowableItem, ViewId, Workspace, item::{self, Item}, @@ -20,8 +19,8 @@ use workspace::{ pub struct DebugSession { remote_id: Option, - running_state: Entity, - label: OnceLock, + pub(crate) running_state: Entity, + pub(crate) quirks: SessionQuirks, stack_trace_view: OnceCell>, _worktree_store: WeakEntity, workspace: WeakEntity, @@ -57,6 +56,7 @@ impl DebugSession { cx, ) }); + let quirks = session.read(cx).quirks(); cx.new(|cx| Self { _subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| { @@ -64,7 +64,7 @@ impl DebugSession { })], remote_id: None, running_state, - label: OnceLock::new(), + quirks, stack_trace_view: OnceCell::new(), _worktree_store: project.read(cx).worktree_store().downgrade(), workspace, @@ -110,65 +110,29 @@ impl DebugSession { .update(cx, |state, cx| state.shutdown(cx)); } - pub(crate) fn label(&self, cx: &App) -> SharedString { - if let Some(label) = self.label.get() { - return label.clone(); - } - - let session = self.running_state.read(cx).session(); - - self.label - .get_or_init(|| session.read(cx).label()) - .to_owned() + pub(crate) fn label(&self, cx: &mut App) -> Option { + let session = self.running_state.read(cx).session().clone(); + session.update(cx, |session, cx| { + let session_label = session.label(); + let quirks = session.quirks(); + let mut single_thread_name = || { + let threads = session.threads(cx); + match threads.as_slice() { + [(thread, _)] => Some(SharedString::from(&thread.name)), + _ => 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 { &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 for DebugSession {} diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 05bca8131ac9734b1635a90c22026424f1c5cf2e..505df09cfb2b47821cb59448801f014f923be8f1 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -427,7 +427,7 @@ async fn test_handle_start_debugging_request( let sessions = workspace .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::(cx).unwrap(); - debug_panel.read(cx).sessions() + debug_panel.read(cx).sessions().collect::>() }) .unwrap(); assert_eq!(sessions.len(), 1); @@ -451,7 +451,7 @@ async fn test_handle_start_debugging_request( .unwrap() .read(cx) .session(cx); - let current_sessions = debug_panel.read(cx).sessions(); + let current_sessions = debug_panel.read(cx).sessions().collect::>(); assert_eq!(active_session, current_sessions[1].read(cx).session(cx)); assert_eq!( active_session.read(cx).parent_session(), @@ -1796,7 +1796,7 @@ async fn test_debug_adapters_shutdown_on_app_quit( let panel = workspace.panel::(cx).unwrap(); panel.read_with(cx, |panel, _| { assert!( - !panel.sessions().is_empty(), + panel.sessions().next().is_some(), "Debug session should be active" ); }); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index f4f4b50dab696f257978a35593e090d4efdff97e..d494088b1379dc3820260ae99a98582cc24db319 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -6,6 +6,7 @@ use super::{ }; use crate::{ InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, + debugger::session::SessionQuirks, project_settings::ProjectSettings, terminals::{SshCommand, wrap_for_ssh}, worktree_store::WorktreeStore, @@ -385,10 +386,11 @@ impl DapStore { pub fn new_session( &mut self, - label: SharedString, + label: Option, adapter: DebugAdapterName, task_context: TaskContext, parent_session: Option>, + quirks: SessionQuirks, cx: &mut Context, ) -> Entity { let session_id = SessionId(util::post_inc(&mut self.next_session_id)); @@ -406,6 +408,7 @@ impl DapStore { label, adapter, task_context, + quirks, cx, ); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 59c35da4cac4328dc109b8463ef02868b4885d63..59feb504c55c05af031742b3d97c203c465b9f93 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -151,6 +151,12 @@ pub struct RunningMode { messages_tx: UnboundedSender, } +#[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 { dap::Source { name: abs_path @@ -656,7 +662,7 @@ pub struct OutputToken(pub usize); pub struct Session { pub mode: Mode, id: SessionId, - label: SharedString, + label: Option, adapter: DebugAdapterName, pub(super) capabilities: Capabilities, child_session_ids: HashSet, @@ -679,6 +685,7 @@ pub struct Session { background_tasks: Vec>, restart_task: Option>, task_context: TaskContext, + quirks: SessionQuirks, } trait CacheableCommand: Any + Send + Sync { @@ -792,9 +799,10 @@ impl Session { breakpoint_store: Entity, session_id: SessionId, parent_session: Option>, - label: SharedString, + label: Option, adapter: DebugAdapterName, task_context: TaskContext, + quirks: SessionQuirks, cx: &mut App, ) -> Entity { cx.new::(|cx| { @@ -848,6 +856,7 @@ impl Session { label, adapter, task_context, + quirks, }; this @@ -1022,7 +1031,7 @@ impl Session { self.adapter.clone() } - pub fn label(&self) -> SharedString { + pub fn label(&self) -> Option { self.label.clone() } @@ -2481,4 +2490,8 @@ impl Session { pub fn thread_state(&self, thread_id: ThreadId) -> Option { self.thread_states.thread_state(thread_id) } + + pub fn quirks(&self) -> SessionQuirks { + self.quirks + } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0ec9bac33f89c81527e520322555d6d1071273b4..15869fc5acc4d1fac8777cf957ed7676f4f1a426 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4301,6 +4301,7 @@ impl ProjectPanel { .collect::>(); let components_len = components.len(); + // TODO this can underflow let active_index = components_len - 1 - folded_ancestors.current_ancestor_depth;