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::{client::SessionId, debugger_settings::DebuggerSettings, Capabilities, Thread};
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 div, h_flex, v_flex, ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context,
19 ContextMenu, Disableable, Divider, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize,
20 Indicator, InteractiveElement, IntoElement, Label, ParentElement, Render, SharedString,
21 StatefulInteractiveElement, Styled, Tooltip, Window,
22};
23use util::ResultExt;
24use variable_list::VariableList;
25use workspace::Workspace;
26
27pub struct RunningState {
28 session: Entity<Session>,
29 thread_id: Option<ThreadId>,
30 console: Entity<console::Console>,
31 focus_handle: FocusHandle,
32 _remote_id: Option<ViewId>,
33 show_console_indicator: bool,
34 module_list: Entity<module_list::ModuleList>,
35 active_thread_item: ThreadItem,
36 workspace: WeakEntity<Workspace>,
37 session_id: SessionId,
38 variable_list: Entity<variable_list::VariableList>,
39 _subscriptions: Vec<Subscription>,
40 stack_frame_list: Entity<stack_frame_list::StackFrameList>,
41 loaded_source_list: Entity<loaded_source_list::LoadedSourceList>,
42}
43
44impl Render for RunningState {
45 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
46 let threads = self.session.update(cx, |this, cx| this.threads(cx));
47 self.select_current_thread(&threads, cx);
48
49 let thread_status = self
50 .thread_id
51 .map(|thread_id| self.session.read(cx).thread_status(thread_id))
52 .unwrap_or(ThreadStatus::Exited);
53
54 let selected_thread_name = threads
55 .iter()
56 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
57 .map(|(thread, _)| thread.name.clone())
58 .unwrap_or("Threads".to_owned());
59
60 self.variable_list.update(cx, |this, cx| {
61 this.disabled(thread_status != ThreadStatus::Stopped, cx);
62 });
63
64 let is_terminated = self.session.read(cx).is_terminated();
65 let active_thread_item = &self.active_thread_item;
66
67 let has_no_threads = threads.is_empty();
68 let capabilities = self.capabilities(cx);
69 let state = cx.entity();
70 h_flex()
71 .when(is_terminated, |this| this.bg(gpui::red()))
72 .key_context("DebugPanelItem")
73 .track_focus(&self.focus_handle(cx))
74 .size_full()
75 .items_start()
76 .child(
77 v_flex()
78 .size_full()
79 .items_start()
80 .child(
81 h_flex()
82 .w_full()
83 .border_b_1()
84 .border_color(cx.theme().colors().border_variant)
85 .justify_between()
86 .child(
87 h_flex()
88 .px_1()
89 .py_0p5()
90 .w_full()
91 .gap_1()
92 .map(|this| {
93 if thread_status == ThreadStatus::Running {
94 this.child(
95 IconButton::new(
96 "debug-pause",
97 IconName::DebugPause,
98 )
99 .icon_size(IconSize::XSmall)
100 .on_click(cx.listener(|this, _, _window, cx| {
101 this.pause_thread(cx);
102 }))
103 .tooltip(move |window, cx| {
104 Tooltip::text("Pause program")(window, cx)
105 }),
106 )
107 } else {
108 this.child(
109 IconButton::new(
110 "debug-continue",
111 IconName::DebugContinue,
112 )
113 .icon_size(IconSize::XSmall)
114 .on_click(cx.listener(|this, _, _window, cx| {
115 this.continue_thread(cx)
116 }))
117 .disabled(thread_status != ThreadStatus::Stopped)
118 .tooltip(move |window, cx| {
119 Tooltip::text("Continue program")(window, cx)
120 }),
121 )
122 }
123 })
124 .child(
125 IconButton::new("debug-restart", IconName::DebugRestart)
126 .icon_size(IconSize::XSmall)
127 .on_click(cx.listener(|this, _, _window, cx| {
128 this.restart_session(cx);
129 }))
130 .disabled(
131 !capabilities
132 .supports_restart_request
133 .unwrap_or_default(),
134 )
135 .tooltip(move |window, cx| {
136 Tooltip::text("Restart")(window, cx)
137 }),
138 )
139 .child(
140 IconButton::new("debug-stop", IconName::DebugStop)
141 .icon_size(IconSize::XSmall)
142 .on_click(cx.listener(|this, _, _window, cx| {
143 this.stop_thread(cx);
144 }))
145 .disabled(
146 thread_status != ThreadStatus::Stopped
147 && thread_status != ThreadStatus::Running,
148 )
149 .tooltip({
150 let label = if capabilities
151 .supports_terminate_threads_request
152 .unwrap_or_default()
153 {
154 "Terminate Thread"
155 } else {
156 "Terminate all Threads"
157 };
158 move |window, cx| Tooltip::text(label)(window, cx)
159 }),
160 )
161 .child(
162 IconButton::new(
163 "debug-disconnect",
164 IconName::DebugDisconnect,
165 )
166 .icon_size(IconSize::XSmall)
167 .on_click(cx.listener(|this, _, _window, cx| {
168 this.disconnect_client(cx);
169 }))
170 .disabled(
171 thread_status == ThreadStatus::Exited
172 || thread_status == ThreadStatus::Ended,
173 )
174 .tooltip(Tooltip::text("Disconnect")),
175 )
176 .child(Divider::vertical())
177 .when(
178 capabilities.supports_step_back.unwrap_or(false),
179 |this| {
180 this.child(
181 IconButton::new(
182 "debug-step-back",
183 IconName::DebugStepBack,
184 )
185 .icon_size(IconSize::XSmall)
186 .on_click(cx.listener(|this, _, _window, cx| {
187 this.step_back(cx);
188 }))
189 .disabled(thread_status != ThreadStatus::Stopped)
190 .tooltip(move |window, cx| {
191 Tooltip::text("Step back")(window, cx)
192 }),
193 )
194 },
195 )
196 .child(
197 IconButton::new("debug-step-over", IconName::DebugStepOver)
198 .icon_size(IconSize::XSmall)
199 .on_click(cx.listener(|this, _, _window, cx| {
200 this.step_over(cx);
201 }))
202 .disabled(thread_status != ThreadStatus::Stopped)
203 .tooltip(move |window, cx| {
204 Tooltip::text("Step over")(window, cx)
205 }),
206 )
207 .child(
208 IconButton::new("debug-step-in", IconName::DebugStepInto)
209 .icon_size(IconSize::XSmall)
210 .on_click(cx.listener(|this, _, _window, cx| {
211 this.step_in(cx);
212 }))
213 .disabled(thread_status != ThreadStatus::Stopped)
214 .tooltip(move |window, cx| {
215 Tooltip::text("Step in")(window, cx)
216 }),
217 )
218 .child(
219 IconButton::new("debug-step-out", IconName::DebugStepOut)
220 .icon_size(IconSize::XSmall)
221 .on_click(cx.listener(|this, _, _window, cx| {
222 this.step_out(cx);
223 }))
224 .disabled(thread_status != ThreadStatus::Stopped)
225 .tooltip(move |window, cx| {
226 Tooltip::text("Step out")(window, cx)
227 }),
228 )
229 .child(Divider::vertical())
230 .child(
231 IconButton::new(
232 "debug-ignore-breakpoints",
233 if self.session.read(cx).breakpoints_enabled() {
234 IconName::DebugBreakpoint
235 } else {
236 IconName::DebugIgnoreBreakpoints
237 },
238 )
239 .icon_size(IconSize::XSmall)
240 .on_click(cx.listener(|this, _, _window, cx| {
241 this.toggle_ignore_breakpoints(cx);
242 }))
243 .disabled(
244 thread_status == ThreadStatus::Exited
245 || thread_status == ThreadStatus::Ended,
246 )
247 .tooltip(
248 move |window, cx| {
249 Tooltip::text("Ignore breakpoints")(window, cx)
250 },
251 ),
252 ),
253 )
254 .child(
255 h_flex()
256 .px_1()
257 .py_0p5()
258 .gap_2()
259 .w_3_4()
260 .justify_end()
261 .child(Label::new("Thread:"))
262 .child(
263 DropdownMenu::new(
264 ("thread-list", self.session_id.0),
265 selected_thread_name,
266 ContextMenu::build(
267 window,
268 cx,
269 move |mut this, _, _| {
270 for (thread, _) in threads {
271 let state = state.clone();
272 let thread_id = thread.id;
273 this = this.entry(
274 thread.name,
275 None,
276 move |_, cx| {
277 state.update(cx, |state, cx| {
278 state.select_thread(
279 ThreadId(thread_id),
280 cx,
281 );
282 });
283 },
284 );
285 }
286 this
287 },
288 ),
289 )
290 .disabled(
291 has_no_threads
292 || thread_status != ThreadStatus::Stopped,
293 ),
294 ),
295 ),
296 )
297 .child(
298 h_flex()
299 .size_full()
300 .items_start()
301 .p_1()
302 .gap_4()
303 .child(self.stack_frame_list.clone()),
304 ),
305 )
306 .child(
307 v_flex()
308 .border_l_1()
309 .border_color(cx.theme().colors().border_variant)
310 .size_full()
311 .items_start()
312 .child(
313 h_flex()
314 .border_b_1()
315 .w_full()
316 .border_color(cx.theme().colors().border_variant)
317 .child(self.render_entry_button(
318 &SharedString::from("Variables"),
319 ThreadItem::Variables,
320 cx,
321 ))
322 .when(
323 capabilities.supports_modules_request.unwrap_or_default(),
324 |this| {
325 this.child(self.render_entry_button(
326 &SharedString::from("Modules"),
327 ThreadItem::Modules,
328 cx,
329 ))
330 },
331 )
332 .when(
333 capabilities
334 .supports_loaded_sources_request
335 .unwrap_or_default(),
336 |this| {
337 this.child(self.render_entry_button(
338 &SharedString::from("Loaded Sources"),
339 ThreadItem::LoadedSource,
340 cx,
341 ))
342 },
343 )
344 .child(self.render_entry_button(
345 &SharedString::from("Console"),
346 ThreadItem::Console,
347 cx,
348 )),
349 )
350 .when(*active_thread_item == ThreadItem::Variables, |this| {
351 this.child(self.variable_list.clone())
352 })
353 .when(*active_thread_item == ThreadItem::Modules, |this| {
354 this.size_full().child(self.module_list.clone())
355 })
356 .when(*active_thread_item == ThreadItem::LoadedSource, |this| {
357 this.size_full().child(self.loaded_source_list.clone())
358 })
359 .when(*active_thread_item == ThreadItem::Console, |this| {
360 this.child(self.console.clone())
361 }),
362 )
363 }
364}
365
366impl RunningState {
367 pub fn new(
368 session: Entity<Session>,
369 workspace: WeakEntity<Workspace>,
370 window: &mut Window,
371 cx: &mut Context<Self>,
372 ) -> Self {
373 let focus_handle = cx.focus_handle();
374 let session_id = session.read(cx).session_id();
375 let weak_state = cx.weak_entity();
376 let stack_frame_list = cx.new(|cx| {
377 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
378 });
379
380 let variable_list =
381 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
382
383 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
384
385 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
386
387 let console = cx.new(|cx| {
388 Console::new(
389 session.clone(),
390 stack_frame_list.clone(),
391 variable_list.clone(),
392 window,
393 cx,
394 )
395 });
396
397 let _subscriptions = vec![
398 cx.observe(&module_list, |_, _, cx| cx.notify()),
399 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
400 match event {
401 SessionEvent::Stopped(thread_id) => {
402 this.workspace
403 .update(cx, |workspace, cx| {
404 workspace.open_panel::<crate::DebugPanel>(window, cx);
405 })
406 .log_err();
407
408 if let Some(thread_id) = thread_id {
409 this.select_thread(*thread_id, cx);
410 }
411 }
412 SessionEvent::Threads => {
413 let threads = this.session.update(cx, |this, cx| this.threads(cx));
414 this.select_current_thread(&threads, cx);
415 }
416 _ => {}
417 }
418 cx.notify()
419 }),
420 ];
421
422 Self {
423 session,
424 console,
425 workspace,
426 module_list,
427 focus_handle,
428 variable_list,
429 _subscriptions,
430 thread_id: None,
431 _remote_id: None,
432 stack_frame_list,
433 loaded_source_list,
434 session_id,
435 show_console_indicator: false,
436 active_thread_item: ThreadItem::Variables,
437 }
438 }
439
440 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
441 if self.thread_id.is_some() {
442 self.stack_frame_list
443 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
444 }
445 }
446
447 pub fn session(&self) -> &Entity<Session> {
448 &self.session
449 }
450
451 pub fn session_id(&self) -> SessionId {
452 self.session_id
453 }
454
455 #[cfg(any(test, feature = "test-support"))]
456 pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
457 self.active_thread_item = thread_item;
458 cx.notify()
459 }
460
461 #[cfg(any(test, feature = "test-support"))]
462 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
463 &self.stack_frame_list
464 }
465
466 #[cfg(any(test, feature = "test-support"))]
467 pub fn console(&self) -> &Entity<Console> {
468 &self.console
469 }
470
471 #[cfg(any(test, feature = "test-support"))]
472 pub fn module_list(&self) -> &Entity<ModuleList> {
473 &self.module_list
474 }
475
476 #[cfg(any(test, feature = "test-support"))]
477 pub fn variable_list(&self) -> &Entity<VariableList> {
478 &self.variable_list
479 }
480
481 #[cfg(any(test, feature = "test-support"))]
482 pub fn are_breakpoints_ignored(&self, cx: &App) -> bool {
483 self.session.read(cx).ignore_breakpoints()
484 }
485
486 pub fn capabilities(&self, cx: &App) -> Capabilities {
487 self.session().read(cx).capabilities().clone()
488 }
489
490 pub fn select_current_thread(
491 &mut self,
492 threads: &Vec<(Thread, ThreadStatus)>,
493 cx: &mut Context<Self>,
494 ) {
495 let selected_thread = self
496 .thread_id
497 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
498 .or_else(|| threads.first());
499
500 let Some((selected_thread, _)) = selected_thread else {
501 return;
502 };
503
504 if Some(ThreadId(selected_thread.id)) != self.thread_id {
505 self.select_thread(ThreadId(selected_thread.id), cx);
506 }
507 }
508
509 #[cfg(any(test, feature = "test-support"))]
510 pub fn selected_thread_id(&self) -> Option<ThreadId> {
511 self.thread_id
512 }
513
514 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
515 self.thread_id
516 .map(|id| self.session().read(cx).thread_status(id))
517 }
518
519 fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
520 if self.thread_id.is_some_and(|id| id == thread_id) {
521 return;
522 }
523
524 self.thread_id = Some(thread_id);
525
526 self.stack_frame_list
527 .update(cx, |list, cx| list.refresh(cx));
528 cx.notify();
529 }
530
531 fn render_entry_button(
532 &self,
533 label: &SharedString,
534 thread_item: ThreadItem,
535 cx: &mut Context<Self>,
536 ) -> AnyElement {
537 let has_indicator =
538 matches!(thread_item, ThreadItem::Console) && self.show_console_indicator;
539
540 div()
541 .id(label.clone())
542 .px_2()
543 .py_1()
544 .cursor_pointer()
545 .border_b_2()
546 .when(self.active_thread_item == thread_item, |this| {
547 this.border_color(cx.theme().colors().border)
548 })
549 .child(
550 h_flex()
551 .child(Button::new(label.clone(), label.clone()))
552 .when(has_indicator, |this| this.child(Indicator::dot())),
553 )
554 .on_click(cx.listener(move |this, _, _window, cx| {
555 this.active_thread_item = thread_item;
556
557 if matches!(this.active_thread_item, ThreadItem::Console) {
558 this.show_console_indicator = false;
559 }
560
561 cx.notify();
562 }))
563 .into_any_element()
564 }
565
566 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
567 let Some(thread_id) = self.thread_id else {
568 return;
569 };
570
571 self.session().update(cx, |state, cx| {
572 state.continue_thread(thread_id, cx);
573 });
574 }
575
576 pub fn step_over(&mut self, cx: &mut Context<Self>) {
577 let Some(thread_id) = self.thread_id else {
578 return;
579 };
580
581 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
582
583 self.session().update(cx, |state, cx| {
584 state.step_over(thread_id, granularity, cx);
585 });
586 }
587
588 pub fn step_in(&mut self, cx: &mut Context<Self>) {
589 let Some(thread_id) = self.thread_id else {
590 return;
591 };
592
593 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
594
595 self.session().update(cx, |state, cx| {
596 state.step_in(thread_id, granularity, cx);
597 });
598 }
599
600 pub fn step_out(&mut self, cx: &mut Context<Self>) {
601 let Some(thread_id) = self.thread_id else {
602 return;
603 };
604
605 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
606
607 self.session().update(cx, |state, cx| {
608 state.step_out(thread_id, granularity, cx);
609 });
610 }
611
612 pub fn step_back(&mut self, cx: &mut Context<Self>) {
613 let Some(thread_id) = self.thread_id else {
614 return;
615 };
616
617 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
618
619 self.session().update(cx, |state, cx| {
620 state.step_back(thread_id, granularity, cx);
621 });
622 }
623
624 pub fn restart_session(&self, cx: &mut Context<Self>) {
625 self.session().update(cx, |state, cx| {
626 state.restart(None, cx);
627 });
628 }
629
630 pub fn pause_thread(&self, cx: &mut Context<Self>) {
631 let Some(thread_id) = self.thread_id else {
632 return;
633 };
634
635 self.session().update(cx, |state, cx| {
636 state.pause_thread(thread_id, cx);
637 });
638 }
639
640 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
641 self.workspace
642 .update(cx, |workspace, cx| {
643 workspace
644 .project()
645 .read(cx)
646 .breakpoint_store()
647 .update(cx, |store, cx| {
648 store.remove_active_position(Some(self.session_id), cx)
649 })
650 })
651 .log_err();
652
653 self.session.update(cx, |session, cx| {
654 session.shutdown(cx).detach();
655 })
656 }
657
658 pub fn stop_thread(&self, cx: &mut Context<Self>) {
659 let Some(thread_id) = self.thread_id else {
660 return;
661 };
662
663 self.workspace
664 .update(cx, |workspace, cx| {
665 workspace
666 .project()
667 .read(cx)
668 .breakpoint_store()
669 .update(cx, |store, cx| {
670 store.remove_active_position(Some(self.session_id), cx)
671 })
672 })
673 .log_err();
674
675 self.session().update(cx, |state, cx| {
676 state.terminate_threads(Some(vec![thread_id; 1]), cx);
677 });
678 }
679
680 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
681 self.session().update(cx, |state, cx| {
682 state.disconnect_client(cx);
683 });
684 }
685
686 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
687 self.session.update(cx, |session, cx| {
688 session.toggle_ignore_breakpoints(cx).detach();
689 });
690 }
691}
692
693impl EventEmitter<DebugPanelItemEvent> for RunningState {}
694
695impl Focusable for RunningState {
696 fn focus_handle(&self, _: &App) -> FocusHandle {
697 self.focus_handle.clone()
698 }
699}