1mod console;
2mod loaded_source_list;
3mod module_list;
4pub mod stack_frame_list;
5pub mod variable_list;
6
7use super::{DebugPanelItemEvent, ThreadItem};
8use console::Console;
9use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
10use gpui::{AppContext, Entity, EventEmitter, FocusHandle, Focusable, Subscription, WeakEntity};
11use loaded_source_list::LoadedSourceList;
12use module_list::ModuleList;
13use project::debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus};
14use rpc::proto::ViewId;
15use settings::Settings;
16use stack_frame_list::StackFrameList;
17use ui::{
18 ActiveTheme, AnyElement, App, Button, Context, ContextMenu, DropdownMenu, FluentBuilder,
19 Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
20 StatefulInteractiveElement, Styled, Window, div, h_flex, v_flex,
21};
22use util::ResultExt;
23use variable_list::VariableList;
24use workspace::Workspace;
25
26pub struct RunningState {
27 session: Entity<Session>,
28 thread_id: Option<ThreadId>,
29 console: Entity<console::Console>,
30 focus_handle: FocusHandle,
31 _remote_id: Option<ViewId>,
32 show_console_indicator: bool,
33 module_list: Entity<module_list::ModuleList>,
34 active_thread_item: ThreadItem,
35 workspace: WeakEntity<Workspace>,
36 session_id: SessionId,
37 variable_list: Entity<variable_list::VariableList>,
38 _subscriptions: Vec<Subscription>,
39 stack_frame_list: Entity<stack_frame_list::StackFrameList>,
40 loaded_source_list: Entity<loaded_source_list::LoadedSourceList>,
41}
42
43impl Render for RunningState {
44 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
45 let threads = self.session.update(cx, |this, cx| this.threads(cx));
46 self.select_current_thread(&threads, cx);
47
48 let thread_status = self
49 .thread_id
50 .map(|thread_id| self.session.read(cx).thread_status(thread_id))
51 .unwrap_or(ThreadStatus::Exited);
52
53 self.variable_list.update(cx, |this, cx| {
54 this.disabled(thread_status != ThreadStatus::Stopped, cx);
55 });
56
57 let active_thread_item = &self.active_thread_item;
58
59 let capabilities = self.capabilities(cx);
60 h_flex()
61 .key_context("DebugPanelItem")
62 .track_focus(&self.focus_handle(cx))
63 .size_full()
64 .items_start()
65 .child(
66 v_flex().size_full().items_start().child(
67 h_flex()
68 .size_full()
69 .items_start()
70 .p_1()
71 .gap_4()
72 .child(self.stack_frame_list.clone()),
73 ),
74 )
75 .child(
76 v_flex()
77 .border_l_1()
78 .border_color(cx.theme().colors().border_variant)
79 .size_full()
80 .items_start()
81 .child(
82 h_flex()
83 .border_b_1()
84 .w_full()
85 .border_color(cx.theme().colors().border_variant)
86 .child(self.render_entry_button(
87 &SharedString::from("Variables"),
88 ThreadItem::Variables,
89 cx,
90 ))
91 .when(
92 capabilities.supports_modules_request.unwrap_or_default(),
93 |this| {
94 this.child(self.render_entry_button(
95 &SharedString::from("Modules"),
96 ThreadItem::Modules,
97 cx,
98 ))
99 },
100 )
101 .when(
102 capabilities
103 .supports_loaded_sources_request
104 .unwrap_or_default(),
105 |this| {
106 this.child(self.render_entry_button(
107 &SharedString::from("Loaded Sources"),
108 ThreadItem::LoadedSource,
109 cx,
110 ))
111 },
112 )
113 .child(self.render_entry_button(
114 &SharedString::from("Console"),
115 ThreadItem::Console,
116 cx,
117 )),
118 )
119 .when(*active_thread_item == ThreadItem::Variables, |this| {
120 this.child(self.variable_list.clone())
121 })
122 .when(*active_thread_item == ThreadItem::Modules, |this| {
123 this.size_full().child(self.module_list.clone())
124 })
125 .when(*active_thread_item == ThreadItem::LoadedSource, |this| {
126 this.size_full().child(self.loaded_source_list.clone())
127 })
128 .when(*active_thread_item == ThreadItem::Console, |this| {
129 this.child(self.console.clone())
130 }),
131 )
132 }
133}
134
135impl RunningState {
136 pub fn new(
137 session: Entity<Session>,
138 workspace: WeakEntity<Workspace>,
139 window: &mut Window,
140 cx: &mut Context<Self>,
141 ) -> Self {
142 let focus_handle = cx.focus_handle();
143 let session_id = session.read(cx).session_id();
144 let weak_state = cx.weak_entity();
145 let stack_frame_list = cx.new(|cx| {
146 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
147 });
148
149 let variable_list =
150 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
151
152 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
153
154 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
155
156 let console = cx.new(|cx| {
157 Console::new(
158 session.clone(),
159 stack_frame_list.clone(),
160 variable_list.clone(),
161 window,
162 cx,
163 )
164 });
165
166 let _subscriptions = vec![
167 cx.observe(&module_list, |_, _, cx| cx.notify()),
168 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
169 match event {
170 SessionEvent::Stopped(thread_id) => {
171 this.workspace
172 .update(cx, |workspace, cx| {
173 workspace.open_panel::<crate::DebugPanel>(window, cx);
174 })
175 .log_err();
176
177 if let Some(thread_id) = thread_id {
178 this.select_thread(*thread_id, cx);
179 }
180 }
181 SessionEvent::Threads => {
182 let threads = this.session.update(cx, |this, cx| this.threads(cx));
183 this.select_current_thread(&threads, cx);
184 }
185 _ => {}
186 }
187 cx.notify()
188 }),
189 ];
190
191 Self {
192 session,
193 console,
194 workspace,
195 module_list,
196 focus_handle,
197 variable_list,
198 _subscriptions,
199 thread_id: None,
200 _remote_id: None,
201 stack_frame_list,
202 loaded_source_list,
203 session_id,
204 show_console_indicator: false,
205 active_thread_item: ThreadItem::Variables,
206 }
207 }
208
209 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
210 if self.thread_id.is_some() {
211 self.stack_frame_list
212 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
213 }
214 }
215
216 pub fn session(&self) -> &Entity<Session> {
217 &self.session
218 }
219
220 pub fn session_id(&self) -> SessionId {
221 self.session_id
222 }
223
224 #[cfg(test)]
225 pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
226 self.active_thread_item = thread_item;
227 cx.notify()
228 }
229
230 #[cfg(test)]
231 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
232 &self.stack_frame_list
233 }
234
235 #[cfg(test)]
236 pub fn console(&self) -> &Entity<Console> {
237 &self.console
238 }
239
240 #[cfg(test)]
241 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
242 &self.module_list
243 }
244
245 #[cfg(test)]
246 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
247 &self.variable_list
248 }
249
250 pub fn capabilities(&self, cx: &App) -> Capabilities {
251 self.session().read(cx).capabilities().clone()
252 }
253
254 pub fn select_current_thread(
255 &mut self,
256 threads: &Vec<(Thread, ThreadStatus)>,
257 cx: &mut Context<Self>,
258 ) {
259 let selected_thread = self
260 .thread_id
261 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
262 .or_else(|| threads.first());
263
264 let Some((selected_thread, _)) = selected_thread else {
265 return;
266 };
267
268 if Some(ThreadId(selected_thread.id)) != self.thread_id {
269 self.select_thread(ThreadId(selected_thread.id), cx);
270 }
271 }
272
273 #[cfg(test)]
274 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
275 self.thread_id
276 }
277
278 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
279 self.thread_id
280 .map(|id| self.session().read(cx).thread_status(id))
281 }
282
283 fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
284 if self.thread_id.is_some_and(|id| id == thread_id) {
285 return;
286 }
287
288 self.thread_id = Some(thread_id);
289
290 self.stack_frame_list
291 .update(cx, |list, cx| list.refresh(cx));
292 cx.notify();
293 }
294
295 fn render_entry_button(
296 &self,
297 label: &SharedString,
298 thread_item: ThreadItem,
299 cx: &mut Context<Self>,
300 ) -> AnyElement {
301 let has_indicator =
302 matches!(thread_item, ThreadItem::Console) && self.show_console_indicator;
303
304 div()
305 .id(label.clone())
306 .px_2()
307 .py_1()
308 .cursor_pointer()
309 .border_b_2()
310 .when(self.active_thread_item == thread_item, |this| {
311 this.border_color(cx.theme().colors().border)
312 })
313 .child(
314 h_flex()
315 .child(Button::new(label.clone(), label.clone()))
316 .when(has_indicator, |this| this.child(Indicator::dot())),
317 )
318 .on_click(cx.listener(move |this, _, _window, cx| {
319 this.active_thread_item = thread_item;
320
321 if matches!(this.active_thread_item, ThreadItem::Console) {
322 this.show_console_indicator = false;
323 }
324
325 cx.notify();
326 }))
327 .into_any_element()
328 }
329
330 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
331 let Some(thread_id) = self.thread_id else {
332 return;
333 };
334
335 self.session().update(cx, |state, cx| {
336 state.continue_thread(thread_id, cx);
337 });
338 }
339
340 pub fn step_over(&mut self, cx: &mut Context<Self>) {
341 let Some(thread_id) = self.thread_id else {
342 return;
343 };
344
345 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
346
347 self.session().update(cx, |state, cx| {
348 state.step_over(thread_id, granularity, cx);
349 });
350 }
351
352 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
353 let Some(thread_id) = self.thread_id else {
354 return;
355 };
356
357 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
358
359 self.session().update(cx, |state, cx| {
360 state.step_in(thread_id, granularity, cx);
361 });
362 }
363
364 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
365 let Some(thread_id) = self.thread_id else {
366 return;
367 };
368
369 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
370
371 self.session().update(cx, |state, cx| {
372 state.step_out(thread_id, granularity, cx);
373 });
374 }
375
376 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
377 let Some(thread_id) = self.thread_id else {
378 return;
379 };
380
381 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
382
383 self.session().update(cx, |state, cx| {
384 state.step_back(thread_id, granularity, cx);
385 });
386 }
387
388 pub fn restart_session(&self, cx: &mut Context<Self>) {
389 self.session().update(cx, |state, cx| {
390 state.restart(None, cx);
391 });
392 }
393
394 pub fn pause_thread(&self, cx: &mut Context<Self>) {
395 let Some(thread_id) = self.thread_id else {
396 return;
397 };
398
399 self.session().update(cx, |state, cx| {
400 state.pause_thread(thread_id, cx);
401 });
402 }
403
404 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
405 self.workspace
406 .update(cx, |workspace, cx| {
407 workspace
408 .project()
409 .read(cx)
410 .breakpoint_store()
411 .update(cx, |store, cx| {
412 store.remove_active_position(Some(self.session_id), cx)
413 })
414 })
415 .log_err();
416
417 self.session.update(cx, |session, cx| {
418 session.shutdown(cx).detach();
419 })
420 }
421
422 pub fn stop_thread(&self, cx: &mut Context<Self>) {
423 let Some(thread_id) = self.thread_id else {
424 return;
425 };
426
427 self.workspace
428 .update(cx, |workspace, cx| {
429 workspace
430 .project()
431 .read(cx)
432 .breakpoint_store()
433 .update(cx, |store, cx| {
434 store.remove_active_position(Some(self.session_id), cx)
435 })
436 })
437 .log_err();
438
439 self.session().update(cx, |state, cx| {
440 state.terminate_threads(Some(vec![thread_id; 1]), cx);
441 });
442 }
443
444 #[expect(
445 unused,
446 reason = "Support for disconnecting a client is not wired through yet"
447 )]
448 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
449 self.session().update(cx, |state, cx| {
450 state.disconnect_client(cx);
451 });
452 }
453
454 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
455 self.session.update(cx, |session, cx| {
456 session.toggle_ignore_breakpoints(cx).detach();
457 });
458 }
459
460 pub(crate) fn thread_dropdown(
461 &self,
462 window: &mut Window,
463 cx: &mut Context<'_, RunningState>,
464 ) -> DropdownMenu {
465 let state = cx.entity();
466 let threads = self.session.update(cx, |this, cx| this.threads(cx));
467 let selected_thread_name = threads
468 .iter()
469 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
470 .map(|(thread, _)| thread.name.clone())
471 .unwrap_or("Threads".to_owned());
472 DropdownMenu::new(
473 ("thread-list", self.session_id.0),
474 selected_thread_name,
475 ContextMenu::build(window, cx, move |mut this, _, _| {
476 for (thread, _) in threads {
477 let state = state.clone();
478 let thread_id = thread.id;
479 this = this.entry(thread.name, None, move |_, cx| {
480 state.update(cx, |state, cx| {
481 state.select_thread(ThreadId(thread_id), cx);
482 });
483 });
484 }
485 this
486 }),
487 )
488 }
489}
490
491impl EventEmitter<DebugPanelItemEvent> for RunningState {}
492
493impl Focusable for RunningState {
494 fn focus_handle(&self, _: &App) -> FocusHandle {
495 self.focus_handle.clone()
496 }
497}