1use std::time::Duration;
2
3use collections::HashMap;
4use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage};
5use project::debugger::session::{ThreadId, ThreadStatus};
6use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
7
8use crate::{
9 debugger_panel::DebugPanel,
10 session::{DebugSession, running::RunningState},
11};
12
13impl DebugPanel {
14 fn dropdown_label(label: impl Into<SharedString>) -> Label {
15 Label::new(label).size(LabelSize::Small)
16 }
17
18 pub fn render_session_menu(
19 &mut self,
20 active_session: Option<Entity<DebugSession>>,
21 running_state: Option<Entity<RunningState>>,
22 window: &mut Window,
23 cx: &mut Context<Self>,
24 ) -> Option<impl IntoElement> {
25 if let Some(running_state) = running_state {
26 let sessions = self.sessions().clone();
27 let weak = cx.weak_entity();
28 let running_state = running_state.read(cx);
29 let label = if let Some(active_session) = active_session.clone() {
30 active_session.read(cx).session(cx).read(cx).label()
31 } else {
32 SharedString::new_static("Unknown Session")
33 };
34
35 let is_terminated = running_state.session().read(cx).is_terminated();
36 let is_started = active_session
37 .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started());
38
39 let session_state_indicator = if is_terminated {
40 Indicator::dot().color(Color::Error).into_any_element()
41 } else if !is_started {
42 Icon::new(IconName::ArrowCircle)
43 .size(IconSize::Small)
44 .color(Color::Muted)
45 .with_animation(
46 "arrow-circle",
47 Animation::new(Duration::from_secs(2)).repeat(),
48 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
49 )
50 .into_any_element()
51 } else {
52 match running_state.thread_status(cx).unwrap_or_default() {
53 ThreadStatus::Stopped => {
54 Indicator::dot().color(Color::Conflict).into_any_element()
55 }
56 _ => Indicator::dot().color(Color::Success).into_any_element(),
57 }
58 };
59
60 let trigger = h_flex()
61 .gap_2()
62 .child(session_state_indicator)
63 .justify_between()
64 .child(
65 DebugPanel::dropdown_label(label)
66 .when(is_terminated, |this| this.strikethrough()),
67 )
68 .into_any_element();
69
70 Some(
71 DropdownMenu::new_with_element(
72 "debugger-session-list",
73 trigger,
74 ContextMenu::build(window, cx, move |mut this, _, cx| {
75 let context_menu = cx.weak_entity();
76 let mut session_depths = HashMap::default();
77 for session in sessions.into_iter() {
78 let weak_session = session.downgrade();
79 let weak_session_id = weak_session.entity_id();
80 let session_id = session.read(cx).session_id(cx);
81 let parent_depth = session
82 .read(cx)
83 .session(cx)
84 .read(cx)
85 .parent_id(cx)
86 .and_then(|parent_id| session_depths.get(&parent_id).cloned());
87 let self_depth =
88 *session_depths.entry(session_id).or_insert_with(|| {
89 parent_depth.map(|depth| depth + 1).unwrap_or(0usize)
90 });
91 this = this.custom_entry(
92 {
93 let weak = weak.clone();
94 let context_menu = context_menu.clone();
95 move |_, cx| {
96 weak_session
97 .read_with(cx, |session, cx| {
98 let context_menu = context_menu.clone();
99
100 let id: SharedString =
101 format!("debug-session-{}", session_id.0)
102 .into();
103
104 h_flex()
105 .w_full()
106 .group(id.clone())
107 .justify_between()
108 .child(session.label_element(self_depth, cx))
109 .child(
110 IconButton::new(
111 "close-debug-session",
112 IconName::Close,
113 )
114 .visible_on_hover(id.clone())
115 .icon_size(IconSize::Small)
116 .on_click({
117 let weak = weak.clone();
118 move |_, window, cx| {
119 weak.update(cx, |panel, cx| {
120 panel.close_session(
121 weak_session_id,
122 window,
123 cx,
124 );
125 })
126 .ok();
127 context_menu
128 .update(cx, |this, cx| {
129 this.cancel(
130 &Default::default(),
131 window,
132 cx,
133 );
134 })
135 .ok();
136 }
137 }),
138 )
139 .into_any_element()
140 })
141 .unwrap_or_else(|_| div().into_any_element())
142 }
143 },
144 {
145 let weak = weak.clone();
146 move |window, cx| {
147 weak.update(cx, |panel, cx| {
148 panel.activate_session(session.clone(), window, cx);
149 })
150 .ok();
151 }
152 },
153 );
154 }
155 this
156 }),
157 )
158 .style(DropdownStyle::Ghost)
159 .handle(self.session_picker_menu_handle.clone()),
160 )
161 } else {
162 None
163 }
164 }
165
166 pub(crate) fn render_thread_dropdown(
167 &self,
168 running_state: &Entity<RunningState>,
169 threads: Vec<(dap::Thread, ThreadStatus)>,
170 window: &mut Window,
171 cx: &mut Context<Self>,
172 ) -> Option<DropdownMenu> {
173 let running_state = running_state.clone();
174 let running_state_read = running_state.read(cx);
175 let thread_id = running_state_read.thread_id();
176 let session = running_state_read.session();
177 let session_id = session.read(cx).session_id();
178 let session_terminated = session.read(cx).is_terminated();
179 let selected_thread_name = threads
180 .iter()
181 .find(|(thread, _)| thread_id.map(|id| id.0) == Some(thread.id))
182 .map(|(thread, _)| {
183 thread
184 .name
185 .is_empty()
186 .then(|| format!("Tid: {}", thread.id))
187 .unwrap_or_else(|| thread.name.clone())
188 });
189
190 if let Some(selected_thread_name) = selected_thread_name {
191 let trigger = DebugPanel::dropdown_label(selected_thread_name).into_any_element();
192 Some(
193 DropdownMenu::new_with_element(
194 ("thread-list", session_id.0),
195 trigger,
196 ContextMenu::build(window, cx, move |mut this, _, _| {
197 for (thread, _) in threads {
198 let running_state = running_state.clone();
199 let thread_id = thread.id;
200 let entry_name = thread
201 .name
202 .is_empty()
203 .then(|| format!("Tid: {}", thread.id))
204 .unwrap_or_else(|| thread.name);
205
206 this = this.entry(entry_name, None, move |window, cx| {
207 running_state.update(cx, |running_state, cx| {
208 running_state.select_thread(ThreadId(thread_id), window, cx);
209 });
210 });
211 }
212 this
213 }),
214 )
215 .disabled(session_terminated)
216 .style(DropdownStyle::Ghost)
217 .handle(self.thread_picker_menu_handle.clone()),
218 )
219 } else {
220 None
221 }
222 }
223}