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}