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