1use std::rc::Rc;
2
3use collections::HashMap;
4use gpui::{Corner, 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 .attach(Corner::BottomLeft)
215 .style(DropdownStyle::Ghost)
216 .handle(self.session_picker_menu_handle.clone());
217
218 Some(menu)
219 }
220
221 fn render_session_menu_entry(
222 weak: WeakEntity<DebugPanel>,
223 context_menu: WeakEntity<ContextMenu>,
224 ancestors: Rc<[WeakEntity<DebugSession>]>,
225 leaf: WeakEntity<DebugSession>,
226 self_depth: usize,
227 _window: &mut Window,
228 cx: &mut App,
229 ) -> AnyElement {
230 let Some(session_entry) = maybe!({
231 let ancestors = ancestors
232 .iter()
233 .map(|ancestor| ancestor.upgrade())
234 .collect::<Option<Vec<_>>>()?;
235 let leaf = leaf.upgrade()?;
236 Some(SessionListEntry { ancestors, leaf })
237 }) else {
238 return div().into_any_element();
239 };
240
241 let id: SharedString = format!(
242 "debug-session-{}",
243 session_entry.leaf.read(cx).session_id(cx).0
244 )
245 .into();
246 let session_entity_id = session_entry.leaf.entity_id();
247
248 h_flex()
249 .w_full()
250 .group(id.clone())
251 .justify_between()
252 .child(session_entry.label_element(self_depth, cx))
253 .child(
254 IconButton::new("close-debug-session", IconName::Close)
255 .visible_on_hover(id)
256 .icon_size(IconSize::Small)
257 .on_click({
258 move |_, window, cx| {
259 weak.update(cx, |panel, cx| {
260 panel.close_session(session_entity_id, window, cx);
261 })
262 .ok();
263 context_menu
264 .update(cx, |this, cx| {
265 this.cancel(&Default::default(), window, cx);
266 })
267 .ok();
268 }
269 }),
270 )
271 .into_any_element()
272 }
273
274 pub(crate) fn render_thread_dropdown(
275 &self,
276 running_state: &Entity<RunningState>,
277 threads: Vec<(dap::Thread, ThreadStatus)>,
278 window: &mut Window,
279 cx: &mut Context<Self>,
280 ) -> Option<DropdownMenu> {
281 const MAX_LABEL_CHARS: usize = 150;
282
283 let running_state = running_state.clone();
284 let running_state_read = running_state.read(cx);
285 let thread_id = running_state_read.thread_id();
286 let session = running_state_read.session();
287 let session_id = session.read(cx).session_id();
288 let session_terminated = session.read(cx).is_terminated();
289 let selected_thread_name = threads
290 .iter()
291 .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
292 .map(|(thread, _)| {
293 thread
294 .name
295 .is_empty()
296 .then(|| format!("Tid: {}", thread.id))
297 .unwrap_or_else(|| thread.name.clone())
298 });
299
300 if let Some(selected_thread_name) = selected_thread_name {
301 let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
302 Some(
303 DropdownMenu::new_with_element(
304 ("thread-list", session_id.0),
305 trigger,
306 ContextMenu::build(window, cx, move |mut this, _, _| {
307 for (thread, _) in threads {
308 let running_state = running_state.clone();
309 let thread_id = thread.id;
310 let entry_name = thread
311 .name
312 .is_empty()
313 .then(|| format!("Tid: {}", thread.id))
314 .unwrap_or_else(|| thread.name);
315 let entry_name = truncate_and_trailoff(&entry_name, MAX_LABEL_CHARS);
316
317 this = this.entry(entry_name, None, move |window, cx| {
318 running_state.update(cx, |running_state, cx| {
319 running_state.select_thread(ThreadId(thread_id), window, cx);
320 });
321 });
322 }
323 this
324 }),
325 )
326 .attach(Corner::BottomLeft)
327 .disabled(session_terminated)
328 .style(DropdownStyle::Ghost)
329 .handle(self.thread_picker_menu_handle.clone()),
330 )
331 } else {
332 None
333 }
334 }
335}