1use crate::{
2 ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
3 StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
4};
5use crate::{new_session_modal::NewSessionModal, session::DebugSession};
6use anyhow::{Result, anyhow};
7use collections::HashMap;
8use command_palette_hooks::CommandPaletteFilter;
9use dap::StartDebuggingRequestArguments;
10use dap::{
11 ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
12 client::SessionId, debugger_settings::DebuggerSettings,
13};
14use futures::{SinkExt as _, channel::mpsc};
15use gpui::{
16 Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
17 FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
18 actions, anchored, deferred,
19};
20
21use project::debugger::session::{Session, SessionStateEvent};
22use project::{
23 Project,
24 debugger::{
25 dap_store::{self, DapStore},
26 session::ThreadStatus,
27 },
28 terminals::TerminalKind,
29};
30use rpc::proto::{self};
31use settings::Settings;
32use std::any::TypeId;
33use std::path::Path;
34use std::sync::Arc;
35use task::{DebugTaskDefinition, DebugTaskTemplate};
36use terminal_view::terminal_panel::TerminalPanel;
37use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
38use workspace::{
39 Workspace,
40 dock::{DockPosition, Panel, PanelEvent},
41};
42
43pub enum DebugPanelEvent {
44 Exited(SessionId),
45 Terminated(SessionId),
46 Stopped {
47 client_id: SessionId,
48 event: StoppedEvent,
49 go_to_stack_frame: bool,
50 },
51 Thread((SessionId, ThreadEvent)),
52 Continued((SessionId, ContinuedEvent)),
53 Output((SessionId, OutputEvent)),
54 Module((SessionId, ModuleEvent)),
55 LoadedSource((SessionId, LoadedSourceEvent)),
56 ClientShutdown(SessionId),
57 CapabilitiesChanged(SessionId),
58}
59
60actions!(debug_panel, [ToggleFocus]);
61pub struct DebugPanel {
62 size: Pixels,
63 sessions: Vec<Entity<DebugSession>>,
64 active_session: Option<Entity<DebugSession>>,
65 /// This represents the last debug definition that was created in the new session modal
66 pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
67 project: Entity<Project>,
68 workspace: WeakEntity<Workspace>,
69 focus_handle: FocusHandle,
70 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
71 _subscriptions: Vec<Subscription>,
72}
73
74impl DebugPanel {
75 pub fn new(
76 workspace: &Workspace,
77 window: &mut Window,
78 cx: &mut Context<Workspace>,
79 ) -> Entity<Self> {
80 cx.new(|cx| {
81 let project = workspace.project().clone();
82 let dap_store = project.read(cx).dap_store();
83
84 let weak = cx.weak_entity();
85
86 let modal_subscription =
87 cx.observe_new::<tasks_ui::TasksModal>(move |_, window, cx| {
88 let modal_entity = cx.entity();
89
90 weak.update(cx, |_: &mut DebugPanel, cx| {
91 let Some(window) = window else {
92 log::error!("Debug panel couldn't subscribe to tasks modal because there was no window");
93 return;
94 };
95
96 cx.subscribe_in(
97 &modal_entity,
98 window,
99 |panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
100 panel.workspace.update(cx, |workspace, cx| {
101 let workspace_handle = cx.entity().clone();
102 workspace.toggle_modal(window, cx, |window, cx| {
103 crate::attach_modal::AttachModal::new(
104 workspace_handle,
105 event.debug_config.clone(),
106 true,
107 window,
108 cx,
109 )
110 });
111 }).ok();
112 },
113 )
114 .detach();
115 })
116 .ok();
117 });
118
119 let _subscriptions = vec![
120 cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
121 modal_subscription,
122 ];
123
124 let debug_panel = Self {
125 size: px(300.),
126 sessions: vec![],
127 active_session: None,
128 _subscriptions,
129 past_debug_definition: None,
130 focus_handle: cx.focus_handle(),
131 project,
132 workspace: workspace.weak_handle(),
133 context_menu: None,
134 };
135
136 debug_panel
137 })
138 }
139
140 fn filter_action_types(&self, cx: &mut App) {
141 let (has_active_session, supports_restart, support_step_back, status) = self
142 .active_session()
143 .map(|item| {
144 let running = item.read(cx).mode().as_running().cloned();
145
146 match running {
147 Some(running) => {
148 let caps = running.read(cx).capabilities(cx);
149 (
150 !running.read(cx).session().read(cx).is_terminated(),
151 caps.supports_restart_request.unwrap_or_default(),
152 caps.supports_step_back.unwrap_or_default(),
153 running.read(cx).thread_status(cx),
154 )
155 }
156 None => (false, false, false, None),
157 }
158 })
159 .unwrap_or((false, false, false, None));
160
161 let filter = CommandPaletteFilter::global_mut(cx);
162 let debugger_action_types = [
163 TypeId::of::<Disconnect>(),
164 TypeId::of::<Stop>(),
165 TypeId::of::<ToggleIgnoreBreakpoints>(),
166 ];
167
168 let running_action_types = [TypeId::of::<Pause>()];
169
170 let stopped_action_type = [
171 TypeId::of::<Continue>(),
172 TypeId::of::<StepOver>(),
173 TypeId::of::<StepInto>(),
174 TypeId::of::<StepOut>(),
175 TypeId::of::<editor::actions::DebuggerRunToCursor>(),
176 TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
177 ];
178
179 let step_back_action_type = [TypeId::of::<StepBack>()];
180 let restart_action_type = [TypeId::of::<Restart>()];
181
182 if has_active_session {
183 filter.show_action_types(debugger_action_types.iter());
184
185 if supports_restart {
186 filter.show_action_types(restart_action_type.iter());
187 } else {
188 filter.hide_action_types(&restart_action_type);
189 }
190
191 if support_step_back {
192 filter.show_action_types(step_back_action_type.iter());
193 } else {
194 filter.hide_action_types(&step_back_action_type);
195 }
196
197 match status {
198 Some(ThreadStatus::Running) => {
199 filter.show_action_types(running_action_types.iter());
200 filter.hide_action_types(&stopped_action_type);
201 }
202 Some(ThreadStatus::Stopped) => {
203 filter.show_action_types(stopped_action_type.iter());
204 filter.hide_action_types(&running_action_types);
205 }
206 _ => {
207 filter.hide_action_types(&running_action_types);
208 filter.hide_action_types(&stopped_action_type);
209 }
210 }
211 } else {
212 // show only the `debug: start`
213 filter.hide_action_types(&debugger_action_types);
214 filter.hide_action_types(&step_back_action_type);
215 filter.hide_action_types(&restart_action_type);
216 filter.hide_action_types(&running_action_types);
217 filter.hide_action_types(&stopped_action_type);
218 }
219 }
220
221 pub fn load(
222 workspace: WeakEntity<Workspace>,
223 cx: &mut AsyncWindowContext,
224 ) -> Task<Result<Entity<Self>>> {
225 cx.spawn(async move |cx| {
226 workspace.update_in(cx, |workspace, window, cx| {
227 let debug_panel = DebugPanel::new(workspace, window, cx);
228
229 workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
230 workspace.project().read(cx).breakpoint_store().update(
231 cx,
232 |breakpoint_store, cx| {
233 breakpoint_store.clear_breakpoints(cx);
234 },
235 )
236 });
237
238 cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
239 Self::filter_action_types(debug_panel, cx);
240 })
241 .detach();
242
243 cx.observe(&debug_panel, |_, debug_panel, cx| {
244 debug_panel.update(cx, |debug_panel, cx| {
245 Self::filter_action_types(debug_panel, cx);
246 });
247 })
248 .detach();
249 workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
250
251 debug_panel
252 })
253 })
254 }
255
256 pub fn start_session(
257 &mut self,
258 definition: DebugTaskDefinition,
259 window: &mut Window,
260 cx: &mut Context<Self>,
261 ) {
262 let task_contexts = self
263 .workspace
264 .update(cx, |workspace, cx| {
265 tasks_ui::task_contexts(workspace, window, cx)
266 })
267 .ok();
268 let dap_store = self.project.read(cx).dap_store().clone();
269
270 cx.spawn_in(window, async move |this, cx| {
271 let task_context = if let Some(task) = task_contexts {
272 task.await
273 .active_worktree_context
274 .map_or(task::TaskContext::default(), |context| context.1)
275 } else {
276 task::TaskContext::default()
277 };
278
279 let (session, task) = dap_store.update(cx, |dap_store, cx| {
280 let template = DebugTaskTemplate {
281 locator: None,
282 definition: definition.clone(),
283 };
284 let session = if let Some(debug_config) = template
285 .to_zed_format()
286 .resolve_task("debug_task", &task_context)
287 .and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
288 {
289 dap_store.new_session(debug_config.definition, None, cx)
290 } else {
291 dap_store.new_session(definition.clone(), None, cx)
292 };
293
294 (session.clone(), dap_store.boot_session(session, cx))
295 })?;
296
297 match task.await {
298 Err(e) => {
299 this.update(cx, |this, cx| {
300 this.workspace
301 .update(cx, |workspace, cx| {
302 workspace.show_error(&e, cx);
303 })
304 .ok();
305 })
306 .ok();
307
308 session
309 .update(cx, |session, cx| session.shutdown(cx))?
310 .await;
311 }
312 Ok(_) => Self::register_session(this, session, cx).await?,
313 }
314
315 anyhow::Ok(())
316 })
317 .detach_and_log_err(cx);
318 }
319
320 async fn register_session(
321 this: WeakEntity<Self>,
322 session: Entity<Session>,
323 cx: &mut AsyncWindowContext,
324 ) -> Result<()> {
325 let adapter_name = session.update(cx, |session, _| session.adapter_name())?;
326 this.update_in(cx, |_, window, cx| {
327 cx.subscribe_in(
328 &session,
329 window,
330 move |_, session, event: &SessionStateEvent, window, cx| match event {
331 SessionStateEvent::Restart => {
332 let mut curr_session = session.clone();
333 while let Some(parent_session) = curr_session
334 .read_with(cx, |session, _| session.parent_session().cloned())
335 {
336 curr_session = parent_session;
337 }
338
339 let definition = curr_session.update(cx, |session, _| session.definition());
340 let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
341
342 let definition = definition.clone();
343 cx.spawn_in(window, async move |this, cx| {
344 task.await;
345
346 this.update_in(cx, |this, window, cx| {
347 this.start_session(definition, window, cx)
348 })
349 })
350 .detach_and_log_err(cx);
351 }
352 _ => {}
353 },
354 )
355 .detach();
356 })
357 .ok();
358
359 let serialized_layout = persistence::get_serialized_pane_layout(adapter_name).await;
360
361 let workspace = this.update_in(cx, |this, window, cx| {
362 this.sessions.retain(|session| {
363 session
364 .read(cx)
365 .mode()
366 .as_running()
367 .map_or(false, |running_state| {
368 !running_state.read(cx).session().read(cx).is_terminated()
369 })
370 });
371
372 let session_item = DebugSession::running(
373 this.project.clone(),
374 this.workspace.clone(),
375 session,
376 cx.weak_entity(),
377 serialized_layout,
378 window,
379 cx,
380 );
381
382 if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
383 // We might want to make this an event subscription and only notify when a new thread is selected
384 // This is used to filter the command menu correctly
385 cx.observe(&running, |_, _, cx| cx.notify()).detach();
386 }
387
388 this.sessions.push(session_item.clone());
389 this.activate_session(session_item, window, cx);
390 this.workspace.clone()
391 })?;
392
393 workspace.update_in(cx, |workspace, window, cx| {
394 workspace.focus_panel::<Self>(window, cx);
395 })?;
396 Ok(())
397 }
398
399 pub fn start_child_session(
400 &mut self,
401 request: &StartDebuggingRequestArguments,
402 parent_session: Entity<Session>,
403 window: &mut Window,
404 cx: &mut Context<Self>,
405 ) {
406 let Some(worktree) = parent_session.read(cx).worktree() else {
407 log::error!("Attempted to start a child session from non local debug session");
408 return;
409 };
410
411 let dap_store_handle = self.project.read(cx).dap_store().clone();
412 let breakpoint_store = self.project.read(cx).breakpoint_store();
413 let definition = parent_session.read(cx).definition().clone();
414 let mut binary = parent_session.read(cx).binary().clone();
415 binary.request_args = request.clone();
416
417 cx.spawn_in(window, async move |this, cx| {
418 let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
419 let session =
420 dap_store.new_session(definition.clone(), Some(parent_session.clone()), cx);
421
422 let task = session.update(cx, |session, cx| {
423 session.boot(
424 binary,
425 worktree,
426 breakpoint_store,
427 dap_store_handle.downgrade(),
428 cx,
429 )
430 });
431 (session, task)
432 })?;
433
434 match task.await {
435 Err(e) => {
436 this.update(cx, |this, cx| {
437 this.workspace
438 .update(cx, |workspace, cx| {
439 workspace.show_error(&e, cx);
440 })
441 .ok();
442 })
443 .ok();
444
445 session
446 .update(cx, |session, cx| session.shutdown(cx))?
447 .await;
448 }
449 Ok(_) => Self::register_session(this, session, cx).await?,
450 }
451
452 anyhow::Ok(())
453 })
454 .detach_and_log_err(cx);
455 }
456
457 pub fn active_session(&self) -> Option<Entity<DebugSession>> {
458 self.active_session.clone()
459 }
460
461 fn handle_dap_store_event(
462 &mut self,
463 _dap_store: &Entity<DapStore>,
464 event: &dap_store::DapStoreEvent,
465 window: &mut Window,
466 cx: &mut Context<Self>,
467 ) {
468 match event {
469 dap_store::DapStoreEvent::RunInTerminal {
470 title,
471 cwd,
472 command,
473 args,
474 envs,
475 sender,
476 ..
477 } => {
478 self.handle_run_in_terminal_request(
479 title.clone(),
480 cwd.clone(),
481 command.clone(),
482 args.clone(),
483 envs.clone(),
484 sender.clone(),
485 window,
486 cx,
487 )
488 .detach_and_log_err(cx);
489 }
490 dap_store::DapStoreEvent::SpawnChildSession {
491 request,
492 parent_session,
493 } => {
494 self.start_child_session(request, parent_session.clone(), window, cx);
495 }
496 _ => {}
497 }
498 }
499
500 fn handle_run_in_terminal_request(
501 &self,
502 title: Option<String>,
503 cwd: Option<Arc<Path>>,
504 command: Option<String>,
505 args: Vec<String>,
506 envs: HashMap<String, String>,
507 mut sender: mpsc::Sender<Result<u32>>,
508 window: &mut Window,
509 cx: &mut App,
510 ) -> Task<Result<()>> {
511 let terminal_task = self.workspace.update(cx, |workspace, cx| {
512 let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
513 anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
514 });
515
516 let terminal_panel = match terminal_panel {
517 Ok(panel) => panel,
518 Err(err) => return Task::ready(Err(err)),
519 };
520
521 terminal_panel.update(cx, |terminal_panel, cx| {
522 let terminal_task = terminal_panel.add_terminal(
523 TerminalKind::Debug {
524 command,
525 args,
526 envs,
527 cwd,
528 title,
529 },
530 task::RevealStrategy::Never,
531 window,
532 cx,
533 );
534
535 cx.spawn(async move |_, cx| {
536 let pid_task = async move {
537 let terminal = terminal_task.await?;
538
539 terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
540 };
541
542 pid_task.await
543 })
544 })
545 });
546
547 cx.background_spawn(async move {
548 match terminal_task {
549 Ok(pid_task) => match pid_task.await {
550 Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
551 Ok(None) => {
552 sender
553 .send(Err(anyhow!(
554 "Terminal was spawned but PID was not available"
555 )))
556 .await?
557 }
558 Err(error) => sender.send(Err(anyhow!(error))).await?,
559 },
560 Err(error) => sender.send(Err(anyhow!(error))).await?,
561 };
562
563 Ok(())
564 })
565 }
566
567 fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
568 let Some(session) = self
569 .sessions
570 .iter()
571 .find(|other| entity_id == other.entity_id())
572 .cloned()
573 else {
574 return;
575 };
576 session.update(cx, |this, cx| {
577 if let Some(running) = this.mode().as_running() {
578 running.update(cx, |this, cx| {
579 this.serialize_layout(window, cx);
580 });
581 }
582 });
583 let session_id = session.update(cx, |this, cx| this.session_id(cx));
584 let should_prompt = self
585 .project
586 .update(cx, |this, cx| {
587 let session = this.dap_store().read(cx).session_by_id(session_id);
588 session.map(|session| !session.read(cx).is_terminated())
589 })
590 .unwrap_or_default();
591
592 cx.spawn_in(window, async move |this, cx| {
593 if should_prompt {
594 let response = cx.prompt(
595 gpui::PromptLevel::Warning,
596 "This Debug Session is still running. Are you sure you want to terminate it?",
597 None,
598 &["Yes", "No"],
599 );
600 if response.await == Ok(1) {
601 return;
602 }
603 }
604 session.update(cx, |session, cx| session.shutdown(cx)).ok();
605 this.update(cx, |this, cx| {
606 this.sessions.retain(|other| entity_id != other.entity_id());
607
608 if let Some(active_session_id) = this
609 .active_session
610 .as_ref()
611 .map(|session| session.entity_id())
612 {
613 if active_session_id == entity_id {
614 this.active_session = this.sessions.first().cloned();
615 }
616 }
617 cx.notify()
618 })
619 .ok();
620 })
621 .detach();
622 }
623 fn sessions_drop_down_menu(
624 &self,
625 active_session: &Entity<DebugSession>,
626 window: &mut Window,
627 cx: &mut Context<Self>,
628 ) -> DropdownMenu {
629 let sessions = self.sessions.clone();
630 let weak = cx.weak_entity();
631 let label = active_session.read(cx).label_element(cx);
632
633 DropdownMenu::new_with_element(
634 "debugger-session-list",
635 label,
636 ContextMenu::build(window, cx, move |mut this, _, cx| {
637 let context_menu = cx.weak_entity();
638 for session in sessions.into_iter() {
639 let weak_session = session.downgrade();
640 let weak_session_id = weak_session.entity_id();
641
642 this = this.custom_entry(
643 {
644 let weak = weak.clone();
645 let context_menu = context_menu.clone();
646 move |_, cx| {
647 weak_session
648 .read_with(cx, |session, cx| {
649 let context_menu = context_menu.clone();
650 let id: SharedString =
651 format!("debug-session-{}", session.session_id(cx).0)
652 .into();
653 h_flex()
654 .w_full()
655 .group(id.clone())
656 .justify_between()
657 .child(session.label_element(cx))
658 .child(
659 IconButton::new(
660 "close-debug-session",
661 IconName::Close,
662 )
663 .visible_on_hover(id.clone())
664 .icon_size(IconSize::Small)
665 .on_click({
666 let weak = weak.clone();
667 move |_, window, cx| {
668 weak.update(cx, |panel, cx| {
669 panel.close_session(
670 weak_session_id,
671 window,
672 cx,
673 );
674 })
675 .ok();
676 context_menu
677 .update(cx, |this, cx| {
678 this.cancel(
679 &Default::default(),
680 window,
681 cx,
682 );
683 })
684 .ok();
685 }
686 }),
687 )
688 .into_any_element()
689 })
690 .unwrap_or_else(|_| div().into_any_element())
691 }
692 },
693 {
694 let weak = weak.clone();
695 move |window, cx| {
696 weak.update(cx, |panel, cx| {
697 panel.activate_session(session.clone(), window, cx);
698 })
699 .ok();
700 }
701 },
702 );
703 }
704 this
705 }),
706 )
707 }
708
709 fn deploy_context_menu(
710 &mut self,
711 position: Point<Pixels>,
712 window: &mut Window,
713 cx: &mut Context<Self>,
714 ) {
715 if let Some(running_state) = self
716 .active_session
717 .as_ref()
718 .and_then(|session| session.read(cx).mode().as_running().cloned())
719 {
720 let pane_items_status = running_state.read(cx).pane_items_status(cx);
721 let this = cx.weak_entity();
722
723 let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
724 for (item_kind, is_visible) in pane_items_status.into_iter() {
725 menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
726 let this = this.clone();
727 move |window, cx| {
728 this.update(cx, |this, cx| {
729 if let Some(running_state) =
730 this.active_session.as_ref().and_then(|session| {
731 session.read(cx).mode().as_running().cloned()
732 })
733 {
734 running_state.update(cx, |state, cx| {
735 if is_visible {
736 state.remove_pane_item(item_kind, window, cx);
737 } else {
738 state.add_pane_item(item_kind, position, window, cx);
739 }
740 })
741 }
742 })
743 .ok();
744 }
745 });
746 }
747
748 menu
749 });
750
751 window.focus(&context_menu.focus_handle(cx));
752 let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
753 this.context_menu.take();
754 cx.notify();
755 });
756 self.context_menu = Some((context_menu, position, subscription));
757 }
758 }
759
760 fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
761 let active_session = self.active_session.clone();
762
763 Some(
764 h_flex()
765 .border_b_1()
766 .border_color(cx.theme().colors().border)
767 .p_1()
768 .justify_between()
769 .w_full()
770 .child(
771 h_flex().gap_2().w_full().when_some(
772 active_session
773 .as_ref()
774 .and_then(|session| session.read(cx).mode().as_running()),
775 |this, running_session| {
776 let thread_status = running_session
777 .read(cx)
778 .thread_status(cx)
779 .unwrap_or(project::debugger::session::ThreadStatus::Exited);
780 let capabilities = running_session.read(cx).capabilities(cx);
781 this.map(|this| {
782 if thread_status == ThreadStatus::Running {
783 this.child(
784 IconButton::new("debug-pause", IconName::DebugPause)
785 .icon_size(IconSize::XSmall)
786 .shape(ui::IconButtonShape::Square)
787 .on_click(window.listener_for(
788 &running_session,
789 |this, _, _window, cx| {
790 this.pause_thread(cx);
791 },
792 ))
793 .tooltip(move |window, cx| {
794 Tooltip::text("Pause program")(window, cx)
795 }),
796 )
797 } else {
798 this.child(
799 IconButton::new("debug-continue", IconName::DebugContinue)
800 .icon_size(IconSize::XSmall)
801 .shape(ui::IconButtonShape::Square)
802 .on_click(window.listener_for(
803 &running_session,
804 |this, _, _window, cx| this.continue_thread(cx),
805 ))
806 .disabled(thread_status != ThreadStatus::Stopped)
807 .tooltip(move |window, cx| {
808 Tooltip::text("Continue program")(window, cx)
809 }),
810 )
811 }
812 })
813 .child(
814 IconButton::new("debug-step-over", IconName::ArrowRight)
815 .icon_size(IconSize::XSmall)
816 .shape(ui::IconButtonShape::Square)
817 .on_click(window.listener_for(
818 &running_session,
819 |this, _, _window, cx| {
820 this.step_over(cx);
821 },
822 ))
823 .disabled(thread_status != ThreadStatus::Stopped)
824 .tooltip(move |window, cx| {
825 Tooltip::text("Step over")(window, cx)
826 }),
827 )
828 .child(
829 IconButton::new("debug-step-out", IconName::ArrowUpRight)
830 .icon_size(IconSize::XSmall)
831 .shape(ui::IconButtonShape::Square)
832 .on_click(window.listener_for(
833 &running_session,
834 |this, _, _window, cx| {
835 this.step_out(cx);
836 },
837 ))
838 .disabled(thread_status != ThreadStatus::Stopped)
839 .tooltip(move |window, cx| {
840 Tooltip::text("Step out")(window, cx)
841 }),
842 )
843 .child(
844 IconButton::new("debug-step-into", IconName::ArrowDownRight)
845 .icon_size(IconSize::XSmall)
846 .shape(ui::IconButtonShape::Square)
847 .on_click(window.listener_for(
848 &running_session,
849 |this, _, _window, cx| {
850 this.step_in(cx);
851 },
852 ))
853 .disabled(thread_status != ThreadStatus::Stopped)
854 .tooltip(move |window, cx| {
855 Tooltip::text("Step in")(window, cx)
856 }),
857 )
858 .child(Divider::vertical())
859 .child(
860 IconButton::new(
861 "debug-enable-breakpoint",
862 IconName::DebugDisabledBreakpoint,
863 )
864 .icon_size(IconSize::XSmall)
865 .shape(ui::IconButtonShape::Square)
866 .disabled(thread_status != ThreadStatus::Stopped),
867 )
868 .child(
869 IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
870 .icon_size(IconSize::XSmall)
871 .shape(ui::IconButtonShape::Square)
872 .disabled(thread_status != ThreadStatus::Stopped),
873 )
874 .child(
875 IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
876 .icon_size(IconSize::XSmall)
877 .shape(ui::IconButtonShape::Square)
878 .disabled(
879 thread_status == ThreadStatus::Exited
880 || thread_status == ThreadStatus::Ended,
881 )
882 .on_click(window.listener_for(
883 &running_session,
884 |this, _, _window, cx| {
885 this.toggle_ignore_breakpoints(cx);
886 },
887 ))
888 .tooltip(move |window, cx| {
889 Tooltip::text("Disable all breakpoints")(window, cx)
890 }),
891 )
892 .child(Divider::vertical())
893 .child(
894 IconButton::new("debug-restart", IconName::DebugRestart)
895 .icon_size(IconSize::XSmall)
896 .on_click(window.listener_for(
897 &running_session,
898 |this, _, _window, cx| {
899 this.restart_session(cx);
900 },
901 ))
902 .tooltip(move |window, cx| {
903 Tooltip::text("Restart")(window, cx)
904 }),
905 )
906 .child(
907 IconButton::new("debug-stop", IconName::Power)
908 .icon_size(IconSize::XSmall)
909 .on_click(window.listener_for(
910 &running_session,
911 |this, _, _window, cx| {
912 this.stop_thread(cx);
913 },
914 ))
915 .disabled(
916 thread_status != ThreadStatus::Stopped
917 && thread_status != ThreadStatus::Running,
918 )
919 .tooltip({
920 let label = if capabilities
921 .supports_terminate_threads_request
922 .unwrap_or_default()
923 {
924 "Terminate Thread"
925 } else {
926 "Terminate all Threads"
927 };
928 move |window, cx| Tooltip::text(label)(window, cx)
929 }),
930 )
931 },
932 ),
933 )
934 .child(
935 h_flex()
936 .gap_2()
937 .when_some(
938 active_session
939 .as_ref()
940 .and_then(|session| session.read(cx).mode().as_running())
941 .cloned(),
942 |this, session| {
943 this.child(
944 session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
945 )
946 .child(Divider::vertical())
947 },
948 )
949 .when_some(active_session.as_ref(), |this, session| {
950 let context_menu = self.sessions_drop_down_menu(session, window, cx);
951 this.child(context_menu).child(Divider::vertical())
952 })
953 .child(
954 IconButton::new("debug-new-session", IconName::Plus)
955 .icon_size(IconSize::Small)
956 .on_click({
957 let workspace = self.workspace.clone();
958 let weak_panel = cx.weak_entity();
959 let past_debug_definition = self.past_debug_definition.clone();
960 move |_, window, cx| {
961 let weak_panel = weak_panel.clone();
962 let past_debug_definition = past_debug_definition.clone();
963
964 let _ = workspace.update(cx, |this, cx| {
965 let workspace = cx.weak_entity();
966 this.toggle_modal(window, cx, |window, cx| {
967 NewSessionModal::new(
968 past_debug_definition,
969 weak_panel,
970 workspace,
971 window,
972 cx,
973 )
974 });
975 });
976 }
977 })
978 .tooltip(|window, cx| {
979 Tooltip::for_action(
980 "New Debug Session",
981 &CreateDebuggingSession,
982 window,
983 cx,
984 )
985 }),
986 ),
987 ),
988 )
989 }
990
991 fn activate_session(
992 &mut self,
993 session_item: Entity<DebugSession>,
994 window: &mut Window,
995 cx: &mut Context<Self>,
996 ) {
997 debug_assert!(self.sessions.contains(&session_item));
998 session_item.focus_handle(cx).focus(window);
999 session_item.update(cx, |this, cx| {
1000 if let Some(running) = this.mode().as_running() {
1001 running.update(cx, |this, cx| {
1002 this.go_to_selected_stack_frame(window, cx);
1003 });
1004 }
1005 });
1006 self.active_session = Some(session_item);
1007 cx.notify();
1008 }
1009}
1010
1011impl EventEmitter<PanelEvent> for DebugPanel {}
1012impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1013
1014impl Focusable for DebugPanel {
1015 fn focus_handle(&self, _: &App) -> FocusHandle {
1016 self.focus_handle.clone()
1017 }
1018}
1019
1020impl Panel for DebugPanel {
1021 fn persistent_name() -> &'static str {
1022 "DebugPanel"
1023 }
1024
1025 fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
1026 DockPosition::Bottom
1027 }
1028
1029 fn position_is_valid(&self, position: DockPosition) -> bool {
1030 position == DockPosition::Bottom
1031 }
1032
1033 fn set_position(
1034 &mut self,
1035 _position: DockPosition,
1036 _window: &mut Window,
1037 _cx: &mut Context<Self>,
1038 ) {
1039 }
1040
1041 fn size(&self, _window: &Window, _: &App) -> Pixels {
1042 self.size
1043 }
1044
1045 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1046 self.size = size.unwrap();
1047 }
1048
1049 fn remote_id() -> Option<proto::PanelId> {
1050 Some(proto::PanelId::DebugPanel)
1051 }
1052
1053 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1054 Some(IconName::Debug)
1055 }
1056
1057 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1058 if DebuggerSettings::get_global(cx).button {
1059 Some("Debug Panel")
1060 } else {
1061 None
1062 }
1063 }
1064
1065 fn toggle_action(&self) -> Box<dyn Action> {
1066 Box::new(ToggleFocus)
1067 }
1068
1069 fn activation_priority(&self) -> u32 {
1070 9
1071 }
1072 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1073}
1074
1075impl Render for DebugPanel {
1076 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1077 let has_sessions = self.sessions.len() > 0;
1078 debug_assert_eq!(has_sessions, self.active_session.is_some());
1079
1080 if self
1081 .active_session
1082 .as_ref()
1083 .and_then(|session| session.read(cx).mode().as_running().cloned())
1084 .map(|state| state.read(cx).has_open_context_menu(cx))
1085 .unwrap_or(false)
1086 {
1087 self.context_menu.take();
1088 }
1089
1090 v_flex()
1091 .size_full()
1092 .key_context("DebugPanel")
1093 .child(h_flex().children(self.top_controls_strip(window, cx)))
1094 .track_focus(&self.focus_handle(cx))
1095 .when(self.active_session.is_some(), |this| {
1096 this.on_mouse_down(
1097 MouseButton::Right,
1098 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1099 if this
1100 .active_session
1101 .as_ref()
1102 .and_then(|session| {
1103 session.read(cx).mode().as_running().map(|state| {
1104 state.read(cx).has_pane_at_position(event.position)
1105 })
1106 })
1107 .unwrap_or(false)
1108 {
1109 this.deploy_context_menu(event.position, window, cx);
1110 }
1111 }),
1112 )
1113 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1114 deferred(
1115 anchored()
1116 .position(*position)
1117 .anchor(gpui::Corner::TopLeft)
1118 .child(menu.clone()),
1119 )
1120 .with_priority(1)
1121 }))
1122 })
1123 .map(|this| {
1124 if has_sessions {
1125 this.children(self.active_session.clone())
1126 } else {
1127 this.child(
1128 v_flex()
1129 .h_full()
1130 .gap_1()
1131 .items_center()
1132 .justify_center()
1133 .child(
1134 h_flex().child(
1135 Label::new("No Debugging Sessions")
1136 .size(LabelSize::Small)
1137 .color(Color::Muted),
1138 ),
1139 )
1140 .child(
1141 h_flex().flex_shrink().child(
1142 Button::new("spawn-new-session-empty-state", "New Session")
1143 .size(ButtonSize::Large)
1144 .on_click(|_, window, cx| {
1145 window.dispatch_action(
1146 CreateDebuggingSession.boxed_clone(),
1147 cx,
1148 );
1149 }),
1150 ),
1151 ),
1152 )
1153 }
1154 })
1155 .into_any()
1156 }
1157}
1158
1159struct DebuggerProvider(Entity<DebugPanel>);
1160
1161impl workspace::DebuggerProvider for DebuggerProvider {
1162 fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
1163 self.0.update(cx, |_, cx| {
1164 cx.defer_in(window, |this, window, cx| {
1165 this.start_session(definition, window, cx);
1166 })
1167 })
1168 }
1169}