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