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 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) == Some(*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) == Some(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) == Some(*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 top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
340 let active_session = self.active_session.clone();
341
342 Some(
343 h_flex()
344 .border_b_1()
345 .border_color(cx.theme().colors().border)
346 .p_1()
347 .justify_between()
348 .w_full()
349 .child(
350 h_flex().gap_2().w_full().when_some(
351 active_session
352 .as_ref()
353 .and_then(|session| session.read(cx).mode().as_running()),
354 |this, running_session| {
355 let thread_status = running_session
356 .read(cx)
357 .thread_status(cx)
358 .unwrap_or(project::debugger::session::ThreadStatus::Exited);
359 let capabilities = running_session.read(cx).capabilities(cx);
360 this.map(|this| {
361 if thread_status == ThreadStatus::Running {
362 this.child(
363 IconButton::new("debug-pause", IconName::DebugPause)
364 .icon_size(IconSize::XSmall)
365 .shape(ui::IconButtonShape::Square)
366 .on_click(window.listener_for(
367 &running_session,
368 |this, _, _window, cx| {
369 this.pause_thread(cx);
370 },
371 ))
372 .tooltip(move |window, cx| {
373 Tooltip::text("Pause program")(window, cx)
374 }),
375 )
376 } else {
377 this.child(
378 IconButton::new("debug-continue", IconName::DebugContinue)
379 .icon_size(IconSize::XSmall)
380 .shape(ui::IconButtonShape::Square)
381 .on_click(window.listener_for(
382 &running_session,
383 |this, _, _window, cx| this.continue_thread(cx),
384 ))
385 .disabled(thread_status != ThreadStatus::Stopped)
386 .tooltip(move |window, cx| {
387 Tooltip::text("Continue program")(window, cx)
388 }),
389 )
390 }
391 })
392 .child(
393 IconButton::new("debug-step-over", IconName::ArrowRight)
394 .icon_size(IconSize::XSmall)
395 .shape(ui::IconButtonShape::Square)
396 .on_click(window.listener_for(
397 &running_session,
398 |this, _, _window, cx| {
399 this.step_over(cx);
400 },
401 ))
402 .disabled(thread_status != ThreadStatus::Stopped)
403 .tooltip(move |window, cx| {
404 Tooltip::text("Step over")(window, cx)
405 }),
406 )
407 .child(
408 IconButton::new("debug-step-out", IconName::ArrowUpRight)
409 .icon_size(IconSize::XSmall)
410 .shape(ui::IconButtonShape::Square)
411 .on_click(window.listener_for(
412 &running_session,
413 |this, _, _window, cx| {
414 this.step_out(cx);
415 },
416 ))
417 .disabled(thread_status != ThreadStatus::Stopped)
418 .tooltip(move |window, cx| {
419 Tooltip::text("Step out")(window, cx)
420 }),
421 )
422 .child(
423 IconButton::new("debug-step-into", IconName::ArrowDownRight)
424 .icon_size(IconSize::XSmall)
425 .shape(ui::IconButtonShape::Square)
426 .on_click(window.listener_for(
427 &running_session,
428 |this, _, _window, cx| {
429 this.step_in(cx);
430 },
431 ))
432 .disabled(thread_status != ThreadStatus::Stopped)
433 .tooltip(move |window, cx| {
434 Tooltip::text("Step in")(window, cx)
435 }),
436 )
437 .child(Divider::vertical())
438 .child(
439 IconButton::new(
440 "debug-enable-breakpoint",
441 IconName::DebugDisabledBreakpoint,
442 )
443 .icon_size(IconSize::XSmall)
444 .shape(ui::IconButtonShape::Square)
445 .disabled(thread_status != ThreadStatus::Stopped),
446 )
447 .child(
448 IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
449 .icon_size(IconSize::XSmall)
450 .shape(ui::IconButtonShape::Square)
451 .disabled(thread_status != ThreadStatus::Stopped),
452 )
453 .child(
454 IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
455 .icon_size(IconSize::XSmall)
456 .shape(ui::IconButtonShape::Square)
457 .disabled(
458 thread_status == ThreadStatus::Exited
459 || thread_status == ThreadStatus::Ended,
460 )
461 .on_click(window.listener_for(
462 &running_session,
463 |this, _, _window, cx| {
464 this.toggle_ignore_breakpoints(cx);
465 },
466 ))
467 .tooltip(move |window, cx| {
468 Tooltip::text("Disable all breakpoints")(window, cx)
469 }),
470 )
471 .child(Divider::vertical())
472 .child(
473 IconButton::new("debug-restart", IconName::DebugRestart)
474 .icon_size(IconSize::XSmall)
475 .on_click(window.listener_for(
476 &running_session,
477 |this, _, _window, cx| {
478 this.restart_session(cx);
479 },
480 ))
481 .disabled(
482 !capabilities.supports_restart_request.unwrap_or_default(),
483 )
484 .tooltip(move |window, cx| {
485 Tooltip::text("Restart")(window, cx)
486 }),
487 )
488 .child(
489 IconButton::new("debug-stop", IconName::Power)
490 .icon_size(IconSize::XSmall)
491 .on_click(window.listener_for(
492 &running_session,
493 |this, _, _window, cx| {
494 this.stop_thread(cx);
495 },
496 ))
497 .disabled(
498 thread_status != ThreadStatus::Stopped
499 && thread_status != ThreadStatus::Running,
500 )
501 .tooltip({
502 let label = if capabilities
503 .supports_terminate_threads_request
504 .unwrap_or_default()
505 {
506 "Terminate Thread"
507 } else {
508 "Terminate all Threads"
509 };
510 move |window, cx| Tooltip::text(label)(window, cx)
511 }),
512 )
513 },
514 ),
515 )
516 .child(
517 h_flex()
518 .gap_2()
519 .when_some(
520 active_session
521 .as_ref()
522 .and_then(|session| session.read(cx).mode().as_running())
523 .cloned(),
524 |this, session| {
525 this.child(
526 session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
527 )
528 .child(Divider::vertical())
529 },
530 )
531 .when_some(active_session.as_ref(), |this, session| {
532 let sessions = self.sessions.clone();
533 let weak = cx.weak_entity();
534 let label = session.read(cx).label(cx);
535 this.child(DropdownMenu::new(
536 "debugger-session-list",
537 label,
538 ContextMenu::build(window, cx, move |mut this, _, cx| {
539 for item in sessions {
540 let weak = weak.clone();
541 this = this.entry(
542 session.read(cx).label(cx),
543 None,
544 move |window, cx| {
545 weak.update(cx, |panel, cx| {
546 panel.activate_session(
547 item.clone(),
548 window,
549 cx,
550 );
551 })
552 .ok();
553 },
554 );
555 }
556 this
557 }),
558 ))
559 .child(Divider::vertical())
560 })
561 .child(
562 IconButton::new("debug-new-session", IconName::Plus)
563 .icon_size(IconSize::Small)
564 .on_click({
565 let workspace = self.workspace.clone();
566 let weak_panel = cx.weak_entity();
567 let past_debug_definition = self.past_debug_definition.clone();
568 move |_, window, cx| {
569 let weak_panel = weak_panel.clone();
570 let past_debug_definition = past_debug_definition.clone();
571
572 let _ = workspace.update(cx, |this, cx| {
573 let workspace = cx.weak_entity();
574 this.toggle_modal(window, cx, |window, cx| {
575 NewSessionModal::new(
576 past_debug_definition,
577 weak_panel,
578 workspace,
579 window,
580 cx,
581 )
582 });
583 });
584 }
585 })
586 .tooltip(|window, cx| {
587 Tooltip::for_action(
588 "New Debug Session",
589 &CreateDebuggingSession,
590 window,
591 cx,
592 )
593 }),
594 ),
595 ),
596 )
597 }
598
599 fn activate_session(
600 &mut self,
601 session_item: Entity<DebugSession>,
602 window: &mut Window,
603 cx: &mut Context<Self>,
604 ) {
605 debug_assert!(self.sessions.contains(&session_item));
606 session_item.focus_handle(cx).focus(window);
607 session_item.update(cx, |this, cx| {
608 if let Some(running) = this.mode().as_running() {
609 running.update(cx, |this, cx| {
610 this.go_to_selected_stack_frame(window, cx);
611 });
612 }
613 });
614 self.active_session = Some(session_item);
615 cx.notify();
616 }
617}
618
619impl EventEmitter<PanelEvent> for DebugPanel {}
620impl EventEmitter<DebugPanelEvent> for DebugPanel {}
621impl EventEmitter<project::Event> for DebugPanel {}
622
623impl Focusable for DebugPanel {
624 fn focus_handle(&self, _: &App) -> FocusHandle {
625 self.focus_handle.clone()
626 }
627}
628
629impl Panel for DebugPanel {
630 fn persistent_name() -> &'static str {
631 "DebugPanel"
632 }
633
634 fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
635 DockPosition::Bottom
636 }
637
638 fn position_is_valid(&self, position: DockPosition) -> bool {
639 position == DockPosition::Bottom
640 }
641
642 fn set_position(
643 &mut self,
644 _position: DockPosition,
645 _window: &mut Window,
646 _cx: &mut Context<Self>,
647 ) {
648 }
649
650 fn size(&self, _window: &Window, _: &App) -> Pixels {
651 self.size
652 }
653
654 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
655 self.size = size.unwrap();
656 }
657
658 fn remote_id() -> Option<proto::PanelId> {
659 Some(proto::PanelId::DebugPanel)
660 }
661
662 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
663 Some(IconName::Debug)
664 }
665
666 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
667 if DebuggerSettings::get_global(cx).button {
668 Some("Debug Panel")
669 } else {
670 None
671 }
672 }
673
674 fn toggle_action(&self) -> Box<dyn Action> {
675 Box::new(ToggleFocus)
676 }
677
678 fn activation_priority(&self) -> u32 {
679 9
680 }
681 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
682}
683
684impl Render for DebugPanel {
685 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
686 let has_sessions = self.sessions.len() > 0;
687 debug_assert_eq!(has_sessions, self.active_session.is_some());
688
689 v_flex()
690 .size_full()
691 .key_context("DebugPanel")
692 .child(h_flex().children(self.top_controls_strip(window, cx)))
693 .track_focus(&self.focus_handle(cx))
694 .map(|this| {
695 if has_sessions {
696 this.children(self.active_session.clone())
697 } else {
698 this.child(
699 v_flex()
700 .h_full()
701 .gap_1()
702 .items_center()
703 .justify_center()
704 .child(
705 h_flex().child(
706 Label::new("No Debugging Sessions")
707 .size(LabelSize::Small)
708 .color(Color::Muted),
709 ),
710 )
711 .child(
712 h_flex().flex_shrink().child(
713 Button::new("spawn-new-session-empty-state", "New Session")
714 .size(ButtonSize::Large)
715 .on_click(|_, window, cx| {
716 window.dispatch_action(
717 CreateDebuggingSession.boxed_clone(),
718 cx,
719 );
720 }),
721 ),
722 ),
723 )
724 }
725 })
726 .into_any()
727 }
728}