1use std::{rc::Rc, time::Duration};
2
3use collections::HashMap;
4use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage};
5use project::debugger::session::{ThreadId, ThreadStatus};
6use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
7use util::{maybe, truncate_and_trailoff};
8
9use crate::{
10 debugger_panel::DebugPanel,
11 session::{DebugSession, running::RunningState},
12};
13
14struct SessionListEntry {
15 ancestors: Vec<Entity<DebugSession>>,
16 leaf: Entity<DebugSession>,
17}
18
19impl SessionListEntry {
20 pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement {
21 const MAX_LABEL_CHARS: usize = 150;
22
23 let mut label = String::new();
24 for ancestor in &self.ancestors {
25 label.push_str(&ancestor.update(cx, |ancestor, cx| {
26 ancestor.label(cx).unwrap_or("(child)".into())
27 }));
28 label.push_str(" ยป ");
29 }
30 label.push_str(
31 &self
32 .leaf
33 .update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())),
34 );
35 let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS);
36
37 let is_terminated = self
38 .leaf
39 .read(cx)
40 .running_state
41 .read(cx)
42 .session()
43 .read(cx)
44 .is_terminated();
45 let icon = {
46 if is_terminated {
47 Some(Indicator::dot().color(Color::Error))
48 } else {
49 match self
50 .leaf
51 .read(cx)
52 .running_state
53 .read(cx)
54 .thread_status(cx)
55 .unwrap_or_default()
56 {
57 project::debugger::session::ThreadStatus::Stopped => {
58 Some(Indicator::dot().color(Color::Conflict))
59 }
60 _ => Some(Indicator::dot().color(Color::Success)),
61 }
62 }
63 };
64
65 h_flex()
66 .id("session-label")
67 .ml(depth * px(16.0))
68 .gap_2()
69 .when_some(icon, |this, indicator| this.child(indicator))
70 .justify_between()
71 .child(
72 Label::new(label)
73 .size(LabelSize::Small)
74 .when(is_terminated, |this| this.strikethrough()),
75 )
76 .into_any_element()
77 }
78}
79
80impl DebugPanel {
81 fn dropdown_label(label: impl Into<SharedString>) -> Label {
82 const MAX_LABEL_CHARS: usize = 50;
83 let label = truncate_and_trailoff(&label.into(), MAX_LABEL_CHARS);
84 Label::new(label).size(LabelSize::Small)
85 }
86
87 pub fn render_session_menu(
88 &mut self,
89 active_session: Option<Entity<DebugSession>>,
90 running_state: Option<Entity<RunningState>>,
91 window: &mut Window,
92 cx: &mut Context<Self>,
93 ) -> Option<impl IntoElement> {
94 let running_state = running_state?;
95
96 let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3);
97 let mut sessions_with_children = self.sessions_with_children.iter().peekable();
98
99 while let Some((root, children)) = sessions_with_children.next() {
100 let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice())
101 && let Some(single_child) = single_child.upgrade()
102 && single_child.read(cx).quirks.compact
103 {
104 sessions_with_children.next();
105 SessionListEntry {
106 leaf: single_child.clone(),
107 ancestors: vec![root.clone()],
108 }
109 } else {
110 SessionListEntry {
111 leaf: root.clone(),
112 ancestors: Vec::new(),
113 }
114 };
115 session_entries.push(root_entry);
116
117 session_entries.extend(
118 sessions_with_children
119 .by_ref()
120 .take_while(|(session, _)| {
121 session
122 .read(cx)
123 .session(cx)
124 .read(cx)
125 .parent_id(cx)
126 .is_some()
127 })
128 .map(|(session, _)| SessionListEntry {
129 leaf: session.clone(),
130 ancestors: vec![],
131 }),
132 );
133 }
134
135 let weak = cx.weak_entity();
136 let trigger_label = if let Some(active_session) = active_session.clone() {
137 active_session.update(cx, |active_session, cx| {
138 active_session.label(cx).unwrap_or("(child)".into())
139 })
140 } else {
141 SharedString::new_static("Unknown Session")
142 };
143 let running_state = running_state.read(cx);
144
145 let is_terminated = running_state.session().read(cx).is_terminated();
146 let is_started = active_session
147 .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
148
149 let session_state_indicator = if is_terminated {
150 Indicator::dot().color(Color::Error).into_any_element()
151 } else if !is_started {
152 Icon::new(IconName::ArrowCircle)
153 .size(IconSize::Small)
154 .color(Color::Muted)
155 .with_animation(
156 "arrow-circle",
157 Animation::new(Duration::from_secs(2)).repeat(),
158 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
159 )
160 .into_any_element()
161 } else {
162 match running_state.thread_status(cx).unwrap_or_default() {
163 ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
164 _ => Indicator::dot().color(Color::Success).into_any_element(),
165 }
166 };
167
168 let trigger = h_flex()
169 .gap_2()
170 .child(session_state_indicator)
171 .justify_between()
172 .child(
173 DebugPanel::dropdown_label(trigger_label)
174 .when(is_terminated, |this| this.strikethrough()),
175 )
176 .into_any_element();
177
178 let menu = DropdownMenu::new_with_element(
179 "debugger-session-list",
180 trigger,
181 ContextMenu::build(window, cx, move |mut this, _, cx| {
182 let context_menu = cx.weak_entity();
183 let mut session_depths = HashMap::default();
184 for session_entry in session_entries {
185 let session_id = session_entry.leaf.read(cx).session_id(cx);
186 let parent_depth = session_entry
187 .ancestors
188 .first()
189 .unwrap_or(&session_entry.leaf)
190 .read(cx)
191 .session(cx)
192 .read(cx)
193 .parent_id(cx)
194 .and_then(|parent_id| session_depths.get(&parent_id).cloned());
195 let self_depth = *session_depths
196 .entry(session_id)
197 .or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
198 this = this.custom_entry(
199 {
200 let weak = weak.clone();
201 let context_menu = context_menu.clone();
202 let ancestors: Rc<[_]> = session_entry
203 .ancestors
204 .iter()
205 .map(|session| session.downgrade())
206 .collect();
207 let leaf = session_entry.leaf.downgrade();
208 move |window, cx| {
209 Self::render_session_menu_entry(
210 weak.clone(),
211 context_menu.clone(),
212 ancestors.clone(),
213 leaf.clone(),
214 self_depth,
215 window,
216 cx,
217 )
218 }
219 },
220 {
221 let weak = weak.clone();
222 let leaf = session_entry.leaf.clone();
223 move |window, cx| {
224 weak.update(cx, |panel, cx| {
225 panel.activate_session(leaf.clone(), window, cx);
226 })
227 .ok();
228 }
229 },
230 );
231 }
232 this
233 }),
234 )
235 .style(DropdownStyle::Ghost)
236 .handle(self.session_picker_menu_handle.clone());
237
238 Some(menu)
239 }
240
241 fn render_session_menu_entry(
242 weak: WeakEntity<DebugPanel>,
243 context_menu: WeakEntity<ContextMenu>,
244 ancestors: Rc<[WeakEntity<DebugSession>]>,
245 leaf: WeakEntity<DebugSession>,
246 self_depth: usize,
247 _window: &mut Window,
248 cx: &mut App,
249 ) -> AnyElement {
250 let Some(session_entry) = maybe!({
251 let ancestors = ancestors
252 .iter()
253 .map(|ancestor| ancestor.upgrade())
254 .collect::<Option<Vec<_>>>()?;
255 let leaf = leaf.upgrade()?;
256 Some(SessionListEntry { ancestors, leaf })
257 }) else {
258 return div().into_any_element();
259 };
260
261 let id: SharedString = format!(
262 "debug-session-{}",
263 session_entry.leaf.read(cx).session_id(cx).0
264 )
265 .into();
266 let session_entity_id = session_entry.leaf.entity_id();
267
268 h_flex()
269 .w_full()
270 .group(id.clone())
271 .justify_between()
272 .child(session_entry.label_element(self_depth, cx))
273 .child(
274 IconButton::new("close-debug-session", IconName::Close)
275 .visible_on_hover(id)
276 .icon_size(IconSize::Small)
277 .on_click({
278 move |_, window, cx| {
279 weak.update(cx, |panel, cx| {
280 panel.close_session(session_entity_id, window, cx);
281 })
282 .ok();
283 context_menu
284 .update(cx, |this, cx| {
285 this.cancel(&Default::default(), window, cx);
286 })
287 .ok();
288 }
289 }),
290 )
291 .into_any_element()
292 }
293
294 pub(crate) fn render_thread_dropdown(
295 &self,
296 running_state: &Entity<RunningState>,
297 threads: Vec<(dap::Thread, ThreadStatus)>,
298 window: &mut Window,
299 cx: &mut Context<Self>,
300 ) -> Option<DropdownMenu> {
301 const MAX_LABEL_CHARS: usize = 150;
302
303 let running_state = running_state.clone();
304 let running_state_read = running_state.read(cx);
305 let thread_id = running_state_read.thread_id();
306 let session = running_state_read.session();
307 let session_id = session.read(cx).session_id();
308 let session_terminated = session.read(cx).is_terminated();
309 let selected_thread_name = threads
310 .iter()
311 .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
312 .map(|(thread, _)| {
313 thread
314 .name
315 .is_empty()
316 .then(|| format!("Tid: {}", thread.id))
317 .unwrap_or_else(|| thread.name.clone())
318 });
319
320 if let Some(selected_thread_name) = selected_thread_name {
321 let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
322 Some(
323 DropdownMenu::new_with_element(
324 ("thread-list", session_id.0),
325 trigger,
326 ContextMenu::build(window, cx, move |mut this, _, _| {
327 for (thread, _) in threads {
328 let running_state = running_state.clone();
329 let thread_id = thread.id;
330 let entry_name = thread
331 .name
332 .is_empty()
333 .then(|| format!("Tid: {}", thread.id))
334 .unwrap_or_else(|| thread.name);
335 let entry_name = truncate_and_trailoff(&entry_name, MAX_LABEL_CHARS);
336
337 this = this.entry(entry_name, None, move |window, cx| {
338 running_state.update(cx, |running_state, cx| {
339 running_state.select_thread(ThreadId(thread_id), window, cx);
340 });
341 });
342 }
343 this
344 }),
345 )
346 .disabled(session_terminated)
347 .style(DropdownStyle::Ghost)
348 .handle(self.thread_picker_menu_handle.clone()),
349 )
350 } else {
351 None
352 }
353 }
354}