dropdown_menus.rs

  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}