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