diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index b07c541821..aee25fc9e3 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 bd36b07387..0c88f37ff8 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 76c1d1fb7b..a51377cd76 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 f53b6403b2..b806381d25 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 c90a2878e9..184aedafc2 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 f93aceae09..dca15eb052 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 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 mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3); + let mut sessions_with_children = self.sessions_with_children.iter().peekable(); - 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); - let trigger = h_flex() - .gap_2() - .child(session_state_indicator) - .justify_between() - .child( - DebugPanel::dropdown_label(label) - .when(is_terminated, |this| this.strikethrough()), - ) - .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(); - } - }, - ); - } - this + 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![], }), - ) - .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, + 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(); + } + }), + ) + .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 3c4c830b46..73cfef78cc 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 05bca8131a..505df09cfb 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 f4f4b50dab..d494088b13 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 59c35da4ca..59feb504c5 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 0ec9bac33f..15869fc5ac 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;