1use crate::{
2 ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
3 StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
4};
5use crate::{new_session_modal::NewSessionModal, session::DebugSession};
6use anyhow::{Result, anyhow};
7use collections::HashMap;
8use command_palette_hooks::CommandPaletteFilter;
9use dap::{
10 ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
11 client::SessionId, debugger_settings::DebuggerSettings,
12};
13use futures::{SinkExt as _, channel::mpsc};
14use gpui::{
15 Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
16 Subscription, Task, WeakEntity, actions,
17};
18use project::{
19 Project,
20 debugger::{
21 dap_store::{self, DapStore},
22 session::ThreadStatus,
23 },
24 terminals::TerminalKind,
25};
26use rpc::proto::{self};
27use settings::Settings;
28use std::{any::TypeId, path::PathBuf};
29use task::DebugTaskDefinition;
30use terminal_view::terminal_panel::TerminalPanel;
31use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
32use workspace::{
33 Pane, Workspace,
34 dock::{DockPosition, Panel, PanelEvent},
35 pane,
36};
37
38pub enum DebugPanelEvent {
39 Exited(SessionId),
40 Terminated(SessionId),
41 Stopped {
42 client_id: SessionId,
43 event: StoppedEvent,
44 go_to_stack_frame: bool,
45 },
46 Thread((SessionId, ThreadEvent)),
47 Continued((SessionId, ContinuedEvent)),
48 Output((SessionId, OutputEvent)),
49 Module((SessionId, ModuleEvent)),
50 LoadedSource((SessionId, LoadedSourceEvent)),
51 ClientShutdown(SessionId),
52 CapabilitiesChanged(SessionId),
53}
54
55actions!(debug_panel, [ToggleFocus]);
56pub struct DebugPanel {
57 size: Pixels,
58 pane: Entity<Pane>,
59 /// This represents the last debug definition that was created in the new session modal
60 pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
61 project: WeakEntity<Project>,
62 workspace: WeakEntity<Workspace>,
63 _subscriptions: Vec<Subscription>,
64}
65
66impl DebugPanel {
67 pub fn new(
68 workspace: &Workspace,
69 window: &mut Window,
70 cx: &mut Context<Workspace>,
71 ) -> Entity<Self> {
72 cx.new(|cx| {
73 let project = workspace.project().clone();
74 let dap_store = project.read(cx).dap_store();
75 let pane = cx.new(|cx| {
76 let mut pane = Pane::new(
77 workspace.weak_handle(),
78 project.clone(),
79 Default::default(),
80 None,
81 gpui::NoAction.boxed_clone(),
82 window,
83 cx,
84 );
85 pane.set_can_split(None);
86 pane.set_can_navigate(true, cx);
87 pane.display_nav_history_buttons(None);
88 pane.set_should_display_tab_bar(|_window, _cx| false);
89 pane.set_close_pane_if_empty(true, cx);
90
91 pane
92 });
93
94 let _subscriptions = vec![
95 cx.observe(&pane, |_, _, cx| cx.notify()),
96 cx.subscribe_in(&pane, window, Self::handle_pane_event),
97 cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
98 ];
99
100 let debug_panel = Self {
101 pane,
102 size: px(300.),
103 _subscriptions,
104 past_debug_definition: None,
105 project: project.downgrade(),
106 workspace: workspace.weak_handle(),
107 };
108
109 debug_panel
110 })
111 }
112
113 pub fn load(
114 workspace: WeakEntity<Workspace>,
115 cx: AsyncWindowContext,
116 ) -> Task<Result<Entity<Self>>> {
117 cx.spawn(async move |cx| {
118 workspace.update_in(cx, |workspace, window, cx| {
119 let debug_panel = DebugPanel::new(workspace, window, cx);
120
121 workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
122 workspace.project().read(cx).breakpoint_store().update(
123 cx,
124 |breakpoint_store, cx| {
125 breakpoint_store.clear_breakpoints(cx);
126 },
127 )
128 });
129
130 cx.observe(&debug_panel, |_, debug_panel, cx| {
131 let (has_active_session, supports_restart, support_step_back) = debug_panel
132 .update(cx, |this, cx| {
133 this.active_session(cx)
134 .map(|item| {
135 let running = item.read(cx).mode().as_running().cloned();
136
137 match running {
138 Some(running) => {
139 let caps = running.read(cx).capabilities(cx);
140 (
141 true,
142 caps.supports_restart_request.unwrap_or_default(),
143 caps.supports_step_back.unwrap_or_default(),
144 )
145 }
146 None => (false, false, false),
147 }
148 })
149 .unwrap_or((false, false, false))
150 });
151
152 let filter = CommandPaletteFilter::global_mut(cx);
153 let debugger_action_types = [
154 TypeId::of::<Continue>(),
155 TypeId::of::<StepOver>(),
156 TypeId::of::<StepInto>(),
157 TypeId::of::<StepOut>(),
158 TypeId::of::<Stop>(),
159 TypeId::of::<Disconnect>(),
160 TypeId::of::<Pause>(),
161 TypeId::of::<ToggleIgnoreBreakpoints>(),
162 ];
163
164 let step_back_action_type = [TypeId::of::<StepBack>()];
165 let restart_action_type = [TypeId::of::<Restart>()];
166
167 if has_active_session {
168 filter.show_action_types(debugger_action_types.iter());
169
170 if supports_restart {
171 filter.show_action_types(restart_action_type.iter());
172 } else {
173 filter.hide_action_types(&restart_action_type);
174 }
175
176 if support_step_back {
177 filter.show_action_types(step_back_action_type.iter());
178 } else {
179 filter.hide_action_types(&step_back_action_type);
180 }
181 } else {
182 // show only the `debug: start`
183 filter.hide_action_types(&debugger_action_types);
184 filter.hide_action_types(&step_back_action_type);
185 filter.hide_action_types(&restart_action_type);
186 }
187 })
188 .detach();
189
190 debug_panel
191 })
192 })
193 }
194
195 pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> {
196 self.pane
197 .read(cx)
198 .active_item()
199 .and_then(|panel| panel.downcast::<DebugSession>())
200 }
201
202 pub fn debug_panel_items_by_client(
203 &self,
204 client_id: &SessionId,
205 cx: &Context<Self>,
206 ) -> Vec<Entity<DebugSession>> {
207 self.pane
208 .read(cx)
209 .items()
210 .filter_map(|item| item.downcast::<DebugSession>())
211 .filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
212 .map(|item| item.clone())
213 .collect()
214 }
215
216 pub fn debug_panel_item_by_client(
217 &self,
218 client_id: SessionId,
219 cx: &mut Context<Self>,
220 ) -> Option<Entity<DebugSession>> {
221 self.pane
222 .read(cx)
223 .items()
224 .filter_map(|item| item.downcast::<DebugSession>())
225 .find(|item| {
226 let item = item.read(cx);
227
228 item.session_id(cx) == Some(client_id)
229 })
230 }
231
232 fn handle_dap_store_event(
233 &mut self,
234 dap_store: &Entity<DapStore>,
235 event: &dap_store::DapStoreEvent,
236 window: &mut Window,
237 cx: &mut Context<Self>,
238 ) {
239 match event {
240 dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
241 let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
242 return log::error!(
243 "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
244 );
245 };
246
247 let Some(project) = self.project.upgrade() else {
248 return log::error!("Debug Panel out lived it's weak reference to Project");
249 };
250
251 if self.pane.read_with(cx, |pane, cx| {
252 pane.items_of_type::<DebugSession>()
253 .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
254 }) {
255 // We already have an item for this session.
256 return;
257 }
258 let session_item = DebugSession::running(
259 project,
260 self.workspace.clone(),
261 session,
262 cx.weak_entity(),
263 window,
264 cx,
265 );
266
267 self.pane.update(cx, |pane, cx| {
268 pane.add_item(Box::new(session_item), true, true, None, window, cx);
269 window.focus(&pane.focus_handle(cx));
270 cx.notify();
271 });
272 }
273 dap_store::DapStoreEvent::RunInTerminal {
274 title,
275 cwd,
276 command,
277 args,
278 envs,
279 sender,
280 ..
281 } => {
282 self.handle_run_in_terminal_request(
283 title.clone(),
284 cwd.clone(),
285 command.clone(),
286 args.clone(),
287 envs.clone(),
288 sender.clone(),
289 window,
290 cx,
291 )
292 .detach_and_log_err(cx);
293 }
294 _ => {}
295 }
296 }
297
298 fn handle_run_in_terminal_request(
299 &self,
300 title: Option<String>,
301 cwd: PathBuf,
302 command: Option<String>,
303 args: Vec<String>,
304 envs: HashMap<String, String>,
305 mut sender: mpsc::Sender<Result<u32>>,
306 window: &mut Window,
307 cx: &mut App,
308 ) -> Task<Result<()>> {
309 let terminal_task = self.workspace.update(cx, |workspace, cx| {
310 let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
311 anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
312 });
313
314 let terminal_panel = match terminal_panel {
315 Ok(panel) => panel,
316 Err(err) => return Task::ready(Err(err)),
317 };
318
319 terminal_panel.update(cx, |terminal_panel, cx| {
320 let terminal_task = terminal_panel.add_terminal(
321 TerminalKind::Debug {
322 command,
323 args,
324 envs,
325 cwd,
326 title,
327 },
328 task::RevealStrategy::Always,
329 window,
330 cx,
331 );
332
333 cx.spawn(async move |_, cx| {
334 let pid_task = async move {
335 let terminal = terminal_task.await?;
336
337 terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
338 };
339
340 pid_task.await
341 })
342 })
343 });
344
345 cx.background_spawn(async move {
346 match terminal_task {
347 Ok(pid_task) => match pid_task.await {
348 Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
349 Ok(None) => {
350 sender
351 .send(Err(anyhow!(
352 "Terminal was spawned but PID was not available"
353 )))
354 .await?
355 }
356 Err(error) => sender.send(Err(anyhow!(error))).await?,
357 },
358 Err(error) => sender.send(Err(anyhow!(error))).await?,
359 };
360
361 Ok(())
362 })
363 }
364
365 fn handle_pane_event(
366 &mut self,
367 _: &Entity<Pane>,
368 event: &pane::Event,
369 window: &mut Window,
370 cx: &mut Context<Self>,
371 ) {
372 match event {
373 pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
374 pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
375 pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
376 pane::Event::AddItem { item } => {
377 self.workspace
378 .update(cx, |workspace, cx| {
379 item.added_to_pane(workspace, self.pane.clone(), window, cx)
380 })
381 .ok();
382 }
383 pane::Event::RemovedItem { item } => {
384 if let Some(debug_session) = item.downcast::<DebugSession>() {
385 debug_session.update(cx, |session, cx| {
386 session.shutdown(cx);
387 })
388 }
389 }
390 pane::Event::ActivateItem {
391 local: _,
392 focus_changed,
393 } => {
394 if *focus_changed {
395 if let Some(debug_session) = self
396 .pane
397 .read(cx)
398 .active_item()
399 .and_then(|item| item.downcast::<DebugSession>())
400 {
401 if let Some(running) = debug_session
402 .read_with(cx, |session, _| session.mode().as_running().cloned())
403 {
404 running.update(cx, |running, cx| {
405 running.go_to_selected_stack_frame(window, cx);
406 });
407 }
408 }
409 }
410 }
411
412 _ => {}
413 }
414 }
415
416 fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
417 let active_session = self
418 .pane
419 .read(cx)
420 .active_item()
421 .and_then(|item| item.downcast::<DebugSession>());
422 Some(
423 h_flex()
424 .border_b_1()
425 .border_color(cx.theme().colors().border)
426 .p_1()
427 .justify_between()
428 .w_full()
429 .child(
430 h_flex().gap_2().w_full().when_some(
431 active_session
432 .as_ref()
433 .and_then(|session| session.read(cx).mode().as_running()),
434 |this, running_session| {
435 let thread_status = running_session
436 .read(cx)
437 .thread_status(cx)
438 .unwrap_or(project::debugger::session::ThreadStatus::Exited);
439 let capabilities = running_session.read(cx).capabilities(cx);
440 this.map(|this| {
441 if thread_status == ThreadStatus::Running {
442 this.child(
443 IconButton::new("debug-pause", IconName::DebugPause)
444 .icon_size(IconSize::XSmall)
445 .shape(ui::IconButtonShape::Square)
446 .on_click(window.listener_for(
447 &running_session,
448 |this, _, _window, cx| {
449 this.pause_thread(cx);
450 },
451 ))
452 .tooltip(move |window, cx| {
453 Tooltip::text("Pause program")(window, cx)
454 }),
455 )
456 } else {
457 this.child(
458 IconButton::new("debug-continue", IconName::DebugContinue)
459 .icon_size(IconSize::XSmall)
460 .shape(ui::IconButtonShape::Square)
461 .on_click(window.listener_for(
462 &running_session,
463 |this, _, _window, cx| this.continue_thread(cx),
464 ))
465 .disabled(thread_status != ThreadStatus::Stopped)
466 .tooltip(move |window, cx| {
467 Tooltip::text("Continue program")(window, cx)
468 }),
469 )
470 }
471 })
472 .child(
473 IconButton::new("debug-step-over", IconName::ArrowRight)
474 .icon_size(IconSize::XSmall)
475 .shape(ui::IconButtonShape::Square)
476 .on_click(window.listener_for(
477 &running_session,
478 |this, _, _window, cx| {
479 this.step_over(cx);
480 },
481 ))
482 .disabled(thread_status != ThreadStatus::Stopped)
483 .tooltip(move |window, cx| {
484 Tooltip::text("Step over")(window, cx)
485 }),
486 )
487 .child(
488 IconButton::new("debug-step-out", IconName::ArrowUpRight)
489 .icon_size(IconSize::XSmall)
490 .shape(ui::IconButtonShape::Square)
491 .on_click(window.listener_for(
492 &running_session,
493 |this, _, _window, cx| {
494 this.step_out(cx);
495 },
496 ))
497 .disabled(thread_status != ThreadStatus::Stopped)
498 .tooltip(move |window, cx| {
499 Tooltip::text("Step out")(window, cx)
500 }),
501 )
502 .child(
503 IconButton::new("debug-step-into", IconName::ArrowDownRight)
504 .icon_size(IconSize::XSmall)
505 .shape(ui::IconButtonShape::Square)
506 .on_click(window.listener_for(
507 &running_session,
508 |this, _, _window, cx| {
509 this.step_in(cx);
510 },
511 ))
512 .disabled(thread_status != ThreadStatus::Stopped)
513 .tooltip(move |window, cx| {
514 Tooltip::text("Step in")(window, cx)
515 }),
516 )
517 .child(Divider::vertical())
518 .child(
519 IconButton::new(
520 "debug-enable-breakpoint",
521 IconName::DebugDisabledBreakpoint,
522 )
523 .icon_size(IconSize::XSmall)
524 .shape(ui::IconButtonShape::Square)
525 .disabled(thread_status != ThreadStatus::Stopped),
526 )
527 .child(
528 IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
529 .icon_size(IconSize::XSmall)
530 .shape(ui::IconButtonShape::Square)
531 .disabled(thread_status != ThreadStatus::Stopped),
532 )
533 .child(
534 IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
535 .icon_size(IconSize::XSmall)
536 .shape(ui::IconButtonShape::Square)
537 .disabled(
538 thread_status == ThreadStatus::Exited
539 || thread_status == ThreadStatus::Ended,
540 )
541 .on_click(window.listener_for(
542 &running_session,
543 |this, _, _window, cx| {
544 this.toggle_ignore_breakpoints(cx);
545 },
546 ))
547 .tooltip(move |window, cx| {
548 Tooltip::text("Disable all breakpoints")(window, cx)
549 }),
550 )
551 .child(Divider::vertical())
552 .child(
553 IconButton::new("debug-restart", IconName::DebugRestart)
554 .icon_size(IconSize::XSmall)
555 .on_click(window.listener_for(
556 &running_session,
557 |this, _, _window, cx| {
558 this.restart_session(cx);
559 },
560 ))
561 .disabled(
562 !capabilities.supports_restart_request.unwrap_or_default(),
563 )
564 .tooltip(move |window, cx| {
565 Tooltip::text("Restart")(window, cx)
566 }),
567 )
568 .child(
569 IconButton::new("debug-stop", IconName::Power)
570 .icon_size(IconSize::XSmall)
571 .on_click(window.listener_for(
572 &running_session,
573 |this, _, _window, cx| {
574 this.stop_thread(cx);
575 },
576 ))
577 .disabled(
578 thread_status != ThreadStatus::Stopped
579 && thread_status != ThreadStatus::Running,
580 )
581 .tooltip({
582 let label = if capabilities
583 .supports_terminate_threads_request
584 .unwrap_or_default()
585 {
586 "Terminate Thread"
587 } else {
588 "Terminate all Threads"
589 };
590 move |window, cx| Tooltip::text(label)(window, cx)
591 }),
592 )
593 },
594 ),
595 )
596 .child(
597 h_flex()
598 .gap_2()
599 .when_some(
600 active_session
601 .as_ref()
602 .and_then(|session| session.read(cx).mode().as_running())
603 .cloned(),
604 |this, session| {
605 this.child(
606 session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
607 )
608 .child(Divider::vertical())
609 },
610 )
611 .when_some(active_session.as_ref(), |this, session| {
612 let pane = self.pane.downgrade();
613 let label = session.read(cx).label(cx);
614 this.child(DropdownMenu::new(
615 "debugger-session-list",
616 label,
617 ContextMenu::build(window, cx, move |mut this, _, cx| {
618 let sessions = pane
619 .read_with(cx, |pane, _| {
620 pane.items().map(|item| item.boxed_clone()).collect()
621 })
622 .ok()
623 .unwrap_or_else(Vec::new);
624 for (index, item) in sessions.into_iter().enumerate() {
625 if let Some(session) = item.downcast::<DebugSession>() {
626 let pane = pane.clone();
627 this = this.entry(
628 session.read(cx).label(cx),
629 None,
630 move |window, cx| {
631 pane.update(cx, |pane, cx| {
632 pane.activate_item(
633 index, true, true, window, cx,
634 );
635 })
636 .ok();
637 },
638 );
639 }
640 }
641 this
642 }),
643 ))
644 .child(Divider::vertical())
645 })
646 .child(
647 IconButton::new("debug-new-session", IconName::Plus)
648 .icon_size(IconSize::Small)
649 .on_click({
650 let workspace = self.workspace.clone();
651 let weak_panel = cx.weak_entity();
652 let past_debug_definition = self.past_debug_definition.clone();
653 move |_, window, cx| {
654 let weak_panel = weak_panel.clone();
655 let past_debug_definition = past_debug_definition.clone();
656
657 let _ = workspace.update(cx, |this, cx| {
658 let workspace = cx.weak_entity();
659 this.toggle_modal(window, cx, |window, cx| {
660 NewSessionModal::new(
661 past_debug_definition,
662 weak_panel,
663 workspace,
664 window,
665 cx,
666 )
667 });
668 });
669 }
670 })
671 .tooltip(|window, cx| {
672 Tooltip::for_action(
673 "New Debug Session",
674 &CreateDebuggingSession,
675 window,
676 cx,
677 )
678 }),
679 ),
680 ),
681 )
682 }
683}
684
685impl EventEmitter<PanelEvent> for DebugPanel {}
686impl EventEmitter<DebugPanelEvent> for DebugPanel {}
687impl EventEmitter<project::Event> for DebugPanel {}
688
689impl Focusable for DebugPanel {
690 fn focus_handle(&self, cx: &App) -> FocusHandle {
691 self.pane.focus_handle(cx)
692 }
693}
694
695impl Panel for DebugPanel {
696 fn pane(&self) -> Option<Entity<Pane>> {
697 Some(self.pane.clone())
698 }
699
700 fn persistent_name() -> &'static str {
701 "DebugPanel"
702 }
703
704 fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
705 DockPosition::Bottom
706 }
707
708 fn position_is_valid(&self, position: DockPosition) -> bool {
709 position == DockPosition::Bottom
710 }
711
712 fn set_position(
713 &mut self,
714 _position: DockPosition,
715 _window: &mut Window,
716 _cx: &mut Context<Self>,
717 ) {
718 }
719
720 fn size(&self, _window: &Window, _: &App) -> Pixels {
721 self.size
722 }
723
724 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
725 self.size = size.unwrap();
726 }
727
728 fn remote_id() -> Option<proto::PanelId> {
729 Some(proto::PanelId::DebugPanel)
730 }
731
732 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
733 Some(IconName::Debug)
734 }
735
736 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
737 if DebuggerSettings::get_global(cx).button {
738 Some("Debug Panel")
739 } else {
740 None
741 }
742 }
743
744 fn toggle_action(&self) -> Box<dyn Action> {
745 Box::new(ToggleFocus)
746 }
747
748 fn activation_priority(&self) -> u32 {
749 9
750 }
751 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
752}
753
754impl Render for DebugPanel {
755 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
756 let has_sessions = self.pane.read(cx).items_len() > 0;
757 v_flex()
758 .size_full()
759 .key_context("DebugPanel")
760 .child(h_flex().children(self.top_controls_strip(window, cx)))
761 .track_focus(&self.focus_handle(cx))
762 .map(|this| {
763 if has_sessions {
764 this.child(self.pane.clone())
765 } else {
766 this.child(
767 v_flex()
768 .h_full()
769 .gap_1()
770 .items_center()
771 .justify_center()
772 .child(
773 h_flex().child(
774 Label::new("No Debugging Sessions")
775 .size(LabelSize::Small)
776 .color(Color::Muted),
777 ),
778 )
779 .child(
780 h_flex().flex_shrink().child(
781 Button::new("spawn-new-session-empty-state", "New Session")
782 .size(ButtonSize::Large)
783 .on_click(|_, window, cx| {
784 window.dispatch_action(
785 CreateDebuggingSession.boxed_clone(),
786 cx,
787 );
788 }),
789 ),
790 ),
791 )
792 }
793 })
794 .into_any()
795 }
796}