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