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