use std::{rc::Rc, time::Duration}; use collections::HashMap; use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage}; use project::debugger::session::{ThreadId, ThreadStatus}; use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; 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; let label = truncate_and_trailoff(&label.into(), MAX_LABEL_CHARS); Label::new(label).size(LabelSize::Small) } pub fn render_session_menu( &mut self, active_session: Option>, running_state: Option>, window: &mut Window, cx: &mut Context, ) -> Option { 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(); 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 { 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 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) .icon_size(IconSize::Small) .on_click({ 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( &self, running_state: &Entity, threads: Vec<(dap::Thread, ThreadStatus)>, window: &mut Window, cx: &mut Context, ) -> Option { const MAX_LABEL_CHARS: usize = 150; let running_state = running_state.clone(); let running_state_read = running_state.read(cx); let thread_id = running_state_read.thread_id(); let session = running_state_read.session(); let session_id = session.read(cx).session_id(); let session_terminated = session.read(cx).is_terminated(); let selected_thread_name = threads .iter() .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id)) .map(|(thread, _)| { thread .name .is_empty() .then(|| format!("Tid: {}", thread.id)) .unwrap_or_else(|| thread.name.clone()) }); if let Some(selected_thread_name) = selected_thread_name { let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element(); Some( DropdownMenu::new_with_element( ("thread-list", session_id.0), trigger, ContextMenu::build(window, cx, move |mut this, _, _| { for (thread, _) in threads { let running_state = running_state.clone(); let thread_id = thread.id; let entry_name = thread .name .is_empty() .then(|| format!("Tid: {}", thread.id)) .unwrap_or_else(|| thread.name); let entry_name = truncate_and_trailoff(&entry_name, MAX_LABEL_CHARS); this = this.entry(entry_name, None, move |window, cx| { running_state.update(cx, |running_state, cx| { running_state.select_thread(ThreadId(thread_id), window, cx); }); }); } this }), ) .disabled(session_terminated) .style(DropdownStyle::Ghost) .handle(self.thread_picker_menu_handle.clone()), ) } else { None } } }