1use std::rc::Rc;
2
3use collections::HashMap;
4use gpui::{Entity, WeakEntity};
5use project::debugger::session::{ThreadId, ThreadStatus};
6use ui::{CommonAnimationExt, 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_rotate_animation(2)
156 .into_any_element()
157 } else {
158 match running_state.thread_status(cx).unwrap_or_default() {
159 ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(),
160 _ => Indicator::dot().color(Color::Success).into_any_element(),
161 }
162 };
163
164 let trigger = h_flex()
165 .gap_2()
166 .child(session_state_indicator)
167 .justify_between()
168 .child(
169 DebugPanel::dropdown_label(trigger_label)
170 .when(is_terminated, |this| this.strikethrough()),
171 )
172 .into_any_element();
173
174 let menu = DropdownMenu::new_with_element(
175 "debugger-session-list",
176 trigger,
177 ContextMenu::build(window, cx, move |mut this, _, cx| {
178 let context_menu = cx.weak_entity();
179 let mut session_depths = HashMap::default();
180 for session_entry in session_entries {
181 let session_id = session_entry.leaf.read(cx).session_id(cx);
182 let parent_depth = session_entry
183 .ancestors
184 .first()
185 .unwrap_or(&session_entry.leaf)
186 .read(cx)
187 .session(cx)
188 .read(cx)
189 .parent_id(cx)
190 .and_then(|parent_id| session_depths.get(&parent_id).cloned());
191 let self_depth = *session_depths
192 .entry(session_id)
193 .or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize));
194 this = this.custom_entry(
195 {
196 let weak = weak.clone();
197 let context_menu = context_menu.clone();
198 let ancestors: Rc<[_]> = session_entry
199 .ancestors
200 .iter()
201 .map(|session| session.downgrade())
202 .collect();
203 let leaf = session_entry.leaf.downgrade();
204 move |window, cx| {
205 Self::render_session_menu_entry(
206 weak.clone(),
207 context_menu.clone(),
208 ancestors.clone(),
209 leaf.clone(),
210 self_depth,
211 window,
212 cx,
213 )
214 }
215 },
216 {
217 let weak = weak.clone();
218 let leaf = session_entry.leaf.clone();
219 move |window, cx| {
220 weak.update(cx, |panel, cx| {
221 panel.activate_session(leaf.clone(), window, cx);
222 })
223 .ok();
224 }
225 },
226 );
227 }
228 this
229 }),
230 )
231 .style(DropdownStyle::Ghost)
232 .handle(self.session_picker_menu_handle.clone());
233
234 Some(menu)
235 }
236
237 fn render_session_menu_entry(
238 weak: WeakEntity<DebugPanel>,
239 context_menu: WeakEntity<ContextMenu>,
240 ancestors: Rc<[WeakEntity<DebugSession>]>,
241 leaf: WeakEntity<DebugSession>,
242 self_depth: usize,
243 _window: &mut Window,
244 cx: &mut App,
245 ) -> AnyElement {
246 let Some(session_entry) = maybe!({
247 let ancestors = ancestors
248 .iter()
249 .map(|ancestor| ancestor.upgrade())
250 .collect::<Option<Vec<_>>>()?;
251 let leaf = leaf.upgrade()?;
252 Some(SessionListEntry { ancestors, leaf })
253 }) else {
254 return div().into_any_element();
255 };
256
257 let id: SharedString = format!(
258 "debug-session-{}",
259 session_entry.leaf.read(cx).session_id(cx).0
260 )
261 .into();
262 let session_entity_id = session_entry.leaf.entity_id();
263
264 h_flex()
265 .w_full()
266 .group(id.clone())
267 .justify_between()
268 .child(session_entry.label_element(self_depth, cx))
269 .child(
270 IconButton::new("close-debug-session", IconName::Close)
271 .visible_on_hover(id)
272 .icon_size(IconSize::Small)
273 .on_click({
274 move |_, window, cx| {
275 weak.update(cx, |panel, cx| {
276 panel.close_session(session_entity_id, window, cx);
277 })
278 .ok();
279 context_menu
280 .update(cx, |this, cx| {
281 this.cancel(&Default::default(), window, cx);
282 })
283 .ok();
284 }
285 }),
286 )
287 .into_any_element()
288 }
289
290 pub(crate) fn render_thread_dropdown(
291 &self,
292 running_state: &Entity<RunningState>,
293 threads: Vec<(dap::Thread, ThreadStatus)>,
294 window: &mut Window,
295 cx: &mut Context<Self>,
296 ) -> Option<DropdownMenu> {
297 const MAX_LABEL_CHARS: usize = 150;
298
299 let running_state = running_state.clone();
300 let running_state_read = running_state.read(cx);
301 let thread_id = running_state_read.thread_id();
302 let session = running_state_read.session();
303 let session_id = session.read(cx).session_id();
304 let session_terminated = session.read(cx).is_terminated();
305 let selected_thread_name = threads
306 .iter()
307 .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
308 .map(|(thread, _)| {
309 thread
310 .name
311 .is_empty()
312 .then(|| format!("Tid: {}", thread.id))
313 .unwrap_or_else(|| thread.name.clone())
314 });
315
316 if let Some(selected_thread_name) = selected_thread_name {
317 let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
318 Some(
319 DropdownMenu::new_with_element(
320 ("thread-list", session_id.0),
321 trigger,
322 ContextMenu::build(window, cx, move |mut this, _, _| {
323 for (thread, _) in threads {
324 let running_state = running_state.clone();
325 let thread_id = thread.id;
326 let entry_name = thread
327 .name
328 .is_empty()
329 .then(|| format!("Tid: {}", thread.id))
330 .unwrap_or_else(|| thread.name);
331 let entry_name = truncate_and_trailoff(&entry_name, MAX_LABEL_CHARS);
332
333 this = this.entry(entry_name, None, move |window, cx| {
334 running_state.update(cx, |running_state, cx| {
335 running_state.select_thread(ThreadId(thread_id), window, cx);
336 });
337 });
338 }
339 this
340 }),
341 )
342 .disabled(session_terminated)
343 .style(DropdownStyle::Ghost)
344 .handle(self.thread_picker_menu_handle.clone()),
345 )
346 } else {
347 None
348 }
349 }
350}