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::{Context as _, 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
551 .send(Ok(pid.as_u32()))
552 .await
553 .context("task cancelled")?,
554 Ok(None) => {
555 sender
556 .send(Err(anyhow!(
557 "Terminal was spawned but PID was not available"
558 )))
559 .await?
560 }
561 Err(error) => sender.send(Err(anyhow!(error))).await?,
562 },
563 Err(error) => sender.send(Err(anyhow!(error))).await?,
564 };
565
566 Ok(())
567 })
568 }
569
570 fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
571 let Some(session) = self
572 .sessions
573 .iter()
574 .find(|other| entity_id == other.entity_id())
575 .cloned()
576 else {
577 return;
578 };
579 session.update(cx, |this, cx| {
580 if let Some(running) = this.mode().as_running() {
581 running.update(cx, |this, cx| {
582 this.serialize_layout(window, cx);
583 });
584 }
585 });
586 let session_id = session.update(cx, |this, cx| this.session_id(cx));
587 let should_prompt = self
588 .project
589 .update(cx, |this, cx| {
590 let session = this.dap_store().read(cx).session_by_id(session_id);
591 session.map(|session| !session.read(cx).is_terminated())
592 })
593 .unwrap_or_default();
594
595 cx.spawn_in(window, async move |this, cx| {
596 if should_prompt {
597 let response = cx.prompt(
598 gpui::PromptLevel::Warning,
599 "This Debug Session is still running. Are you sure you want to terminate it?",
600 None,
601 &["Yes", "No"],
602 );
603 if response.await == Ok(1) {
604 return;
605 }
606 }
607 session.update(cx, |session, cx| session.shutdown(cx)).ok();
608 this.update(cx, |this, cx| {
609 this.sessions.retain(|other| entity_id != other.entity_id());
610
611 if let Some(active_session_id) = this
612 .active_session
613 .as_ref()
614 .map(|session| session.entity_id())
615 {
616 if active_session_id == entity_id {
617 this.active_session = this.sessions.first().cloned();
618 }
619 }
620 cx.notify()
621 })
622 .ok();
623 })
624 .detach();
625 }
626 fn sessions_drop_down_menu(
627 &self,
628 active_session: &Entity<DebugSession>,
629 window: &mut Window,
630 cx: &mut Context<Self>,
631 ) -> DropdownMenu {
632 let sessions = self.sessions.clone();
633 let weak = cx.weak_entity();
634 let label = active_session.read(cx).label_element(cx);
635
636 DropdownMenu::new_with_element(
637 "debugger-session-list",
638 label,
639 ContextMenu::build(window, cx, move |mut this, _, cx| {
640 let context_menu = cx.weak_entity();
641 for session in sessions.into_iter() {
642 let weak_session = session.downgrade();
643 let weak_session_id = weak_session.entity_id();
644
645 this = this.custom_entry(
646 {
647 let weak = weak.clone();
648 let context_menu = context_menu.clone();
649 move |_, cx| {
650 weak_session
651 .read_with(cx, |session, cx| {
652 let context_menu = context_menu.clone();
653 let id: SharedString =
654 format!("debug-session-{}", session.session_id(cx).0)
655 .into();
656 h_flex()
657 .w_full()
658 .group(id.clone())
659 .justify_between()
660 .child(session.label_element(cx))
661 .child(
662 IconButton::new(
663 "close-debug-session",
664 IconName::Close,
665 )
666 .visible_on_hover(id.clone())
667 .icon_size(IconSize::Small)
668 .on_click({
669 let weak = weak.clone();
670 move |_, window, cx| {
671 weak.update(cx, |panel, cx| {
672 panel.close_session(
673 weak_session_id,
674 window,
675 cx,
676 );
677 })
678 .ok();
679 context_menu
680 .update(cx, |this, cx| {
681 this.cancel(
682 &Default::default(),
683 window,
684 cx,
685 );
686 })
687 .ok();
688 }
689 }),
690 )
691 .into_any_element()
692 })
693 .unwrap_or_else(|_| div().into_any_element())
694 }
695 },
696 {
697 let weak = weak.clone();
698 move |window, cx| {
699 weak.update(cx, |panel, cx| {
700 panel.activate_session(session.clone(), window, cx);
701 })
702 .ok();
703 }
704 },
705 );
706 }
707 this
708 }),
709 )
710 }
711
712 fn deploy_context_menu(
713 &mut self,
714 position: Point<Pixels>,
715 window: &mut Window,
716 cx: &mut Context<Self>,
717 ) {
718 if let Some(running_state) = self
719 .active_session
720 .as_ref()
721 .and_then(|session| session.read(cx).mode().as_running().cloned())
722 {
723 let pane_items_status = running_state.read(cx).pane_items_status(cx);
724 let this = cx.weak_entity();
725
726 let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
727 for (item_kind, is_visible) in pane_items_status.into_iter() {
728 menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
729 let this = this.clone();
730 move |window, cx| {
731 this.update(cx, |this, cx| {
732 if let Some(running_state) =
733 this.active_session.as_ref().and_then(|session| {
734 session.read(cx).mode().as_running().cloned()
735 })
736 {
737 running_state.update(cx, |state, cx| {
738 if is_visible {
739 state.remove_pane_item(item_kind, window, cx);
740 } else {
741 state.add_pane_item(item_kind, position, window, cx);
742 }
743 })
744 }
745 })
746 .ok();
747 }
748 });
749 }
750
751 menu
752 });
753
754 window.focus(&context_menu.focus_handle(cx));
755 let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
756 this.context_menu.take();
757 cx.notify();
758 });
759 self.context_menu = Some((context_menu, position, subscription));
760 }
761 }
762
763 fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
764 let active_session = self.active_session.clone();
765
766 Some(
767 h_flex()
768 .border_b_1()
769 .border_color(cx.theme().colors().border)
770 .p_1()
771 .justify_between()
772 .w_full()
773 .child(
774 h_flex().gap_2().w_full().when_some(
775 active_session
776 .as_ref()
777 .and_then(|session| session.read(cx).mode().as_running()),
778 |this, running_session| {
779 let thread_status = running_session
780 .read(cx)
781 .thread_status(cx)
782 .unwrap_or(project::debugger::session::ThreadStatus::Exited);
783 let capabilities = running_session.read(cx).capabilities(cx);
784 this.map(|this| {
785 if thread_status == ThreadStatus::Running {
786 this.child(
787 IconButton::new("debug-pause", IconName::DebugPause)
788 .icon_size(IconSize::XSmall)
789 .shape(ui::IconButtonShape::Square)
790 .on_click(window.listener_for(
791 &running_session,
792 |this, _, _window, cx| {
793 this.pause_thread(cx);
794 },
795 ))
796 .tooltip(move |window, cx| {
797 Tooltip::text("Pause program")(window, cx)
798 }),
799 )
800 } else {
801 this.child(
802 IconButton::new("debug-continue", IconName::DebugContinue)
803 .icon_size(IconSize::XSmall)
804 .shape(ui::IconButtonShape::Square)
805 .on_click(window.listener_for(
806 &running_session,
807 |this, _, _window, cx| this.continue_thread(cx),
808 ))
809 .disabled(thread_status != ThreadStatus::Stopped)
810 .tooltip(move |window, cx| {
811 Tooltip::text("Continue program")(window, cx)
812 }),
813 )
814 }
815 })
816 .child(
817 IconButton::new("debug-step-over", IconName::ArrowRight)
818 .icon_size(IconSize::XSmall)
819 .shape(ui::IconButtonShape::Square)
820 .on_click(window.listener_for(
821 &running_session,
822 |this, _, _window, cx| {
823 this.step_over(cx);
824 },
825 ))
826 .disabled(thread_status != ThreadStatus::Stopped)
827 .tooltip(move |window, cx| {
828 Tooltip::text("Step over")(window, cx)
829 }),
830 )
831 .child(
832 IconButton::new("debug-step-out", IconName::ArrowUpRight)
833 .icon_size(IconSize::XSmall)
834 .shape(ui::IconButtonShape::Square)
835 .on_click(window.listener_for(
836 &running_session,
837 |this, _, _window, cx| {
838 this.step_out(cx);
839 },
840 ))
841 .disabled(thread_status != ThreadStatus::Stopped)
842 .tooltip(move |window, cx| {
843 Tooltip::text("Step out")(window, cx)
844 }),
845 )
846 .child(
847 IconButton::new("debug-step-into", IconName::ArrowDownRight)
848 .icon_size(IconSize::XSmall)
849 .shape(ui::IconButtonShape::Square)
850 .on_click(window.listener_for(
851 &running_session,
852 |this, _, _window, cx| {
853 this.step_in(cx);
854 },
855 ))
856 .disabled(thread_status != ThreadStatus::Stopped)
857 .tooltip(move |window, cx| {
858 Tooltip::text("Step in")(window, cx)
859 }),
860 )
861 .child(Divider::vertical())
862 .child(
863 IconButton::new(
864 "debug-enable-breakpoint",
865 IconName::DebugDisabledBreakpoint,
866 )
867 .icon_size(IconSize::XSmall)
868 .shape(ui::IconButtonShape::Square)
869 .disabled(thread_status != ThreadStatus::Stopped),
870 )
871 .child(
872 IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
873 .icon_size(IconSize::XSmall)
874 .shape(ui::IconButtonShape::Square)
875 .disabled(thread_status != ThreadStatus::Stopped),
876 )
877 .child(
878 IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
879 .icon_size(IconSize::XSmall)
880 .shape(ui::IconButtonShape::Square)
881 .disabled(
882 thread_status == ThreadStatus::Exited
883 || thread_status == ThreadStatus::Ended,
884 )
885 .on_click(window.listener_for(
886 &running_session,
887 |this, _, _window, cx| {
888 this.toggle_ignore_breakpoints(cx);
889 },
890 ))
891 .tooltip(move |window, cx| {
892 Tooltip::text("Disable all breakpoints")(window, cx)
893 }),
894 )
895 .child(Divider::vertical())
896 .child(
897 IconButton::new("debug-restart", IconName::DebugRestart)
898 .icon_size(IconSize::XSmall)
899 .on_click(window.listener_for(
900 &running_session,
901 |this, _, _window, cx| {
902 this.restart_session(cx);
903 },
904 ))
905 .tooltip(move |window, cx| {
906 Tooltip::text("Restart")(window, cx)
907 }),
908 )
909 .child(
910 IconButton::new("debug-stop", IconName::Power)
911 .icon_size(IconSize::XSmall)
912 .on_click(window.listener_for(
913 &running_session,
914 |this, _, _window, cx| {
915 this.stop_thread(cx);
916 },
917 ))
918 .disabled(
919 thread_status != ThreadStatus::Stopped
920 && thread_status != ThreadStatus::Running,
921 )
922 .tooltip({
923 let label = if capabilities
924 .supports_terminate_threads_request
925 .unwrap_or_default()
926 {
927 "Terminate Thread"
928 } else {
929 "Terminate all Threads"
930 };
931 move |window, cx| Tooltip::text(label)(window, cx)
932 }),
933 )
934 },
935 ),
936 )
937 .child(
938 h_flex()
939 .gap_2()
940 .when_some(
941 active_session
942 .as_ref()
943 .and_then(|session| session.read(cx).mode().as_running())
944 .cloned(),
945 |this, session| {
946 this.child(
947 session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
948 )
949 .child(Divider::vertical())
950 },
951 )
952 .when_some(active_session.as_ref(), |this, session| {
953 let context_menu = self.sessions_drop_down_menu(session, window, cx);
954 this.child(context_menu).child(Divider::vertical())
955 })
956 .child(
957 IconButton::new("debug-new-session", IconName::Plus)
958 .icon_size(IconSize::Small)
959 .on_click({
960 let workspace = self.workspace.clone();
961 let weak_panel = cx.weak_entity();
962 let past_debug_definition = self.past_debug_definition.clone();
963 move |_, window, cx| {
964 let weak_panel = weak_panel.clone();
965 let past_debug_definition = past_debug_definition.clone();
966
967 let _ = workspace.update(cx, |this, cx| {
968 let workspace = cx.weak_entity();
969 this.toggle_modal(window, cx, |window, cx| {
970 NewSessionModal::new(
971 past_debug_definition,
972 weak_panel,
973 workspace,
974 window,
975 cx,
976 )
977 });
978 });
979 }
980 })
981 .tooltip(|window, cx| {
982 Tooltip::for_action(
983 "New Debug Session",
984 &CreateDebuggingSession,
985 window,
986 cx,
987 )
988 }),
989 ),
990 ),
991 )
992 }
993
994 fn activate_session(
995 &mut self,
996 session_item: Entity<DebugSession>,
997 window: &mut Window,
998 cx: &mut Context<Self>,
999 ) {
1000 debug_assert!(self.sessions.contains(&session_item));
1001 session_item.focus_handle(cx).focus(window);
1002 session_item.update(cx, |this, cx| {
1003 if let Some(running) = this.mode().as_running() {
1004 running.update(cx, |this, cx| {
1005 this.go_to_selected_stack_frame(window, cx);
1006 });
1007 }
1008 });
1009 self.active_session = Some(session_item);
1010 cx.notify();
1011 }
1012}
1013
1014impl EventEmitter<PanelEvent> for DebugPanel {}
1015impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1016
1017impl Focusable for DebugPanel {
1018 fn focus_handle(&self, _: &App) -> FocusHandle {
1019 self.focus_handle.clone()
1020 }
1021}
1022
1023impl Panel for DebugPanel {
1024 fn persistent_name() -> &'static str {
1025 "DebugPanel"
1026 }
1027
1028 fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
1029 DockPosition::Bottom
1030 }
1031
1032 fn position_is_valid(&self, position: DockPosition) -> bool {
1033 position == DockPosition::Bottom
1034 }
1035
1036 fn set_position(
1037 &mut self,
1038 _position: DockPosition,
1039 _window: &mut Window,
1040 _cx: &mut Context<Self>,
1041 ) {
1042 }
1043
1044 fn size(&self, _window: &Window, _: &App) -> Pixels {
1045 self.size
1046 }
1047
1048 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1049 self.size = size.unwrap();
1050 }
1051
1052 fn remote_id() -> Option<proto::PanelId> {
1053 Some(proto::PanelId::DebugPanel)
1054 }
1055
1056 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1057 Some(IconName::Debug)
1058 }
1059
1060 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1061 if DebuggerSettings::get_global(cx).button {
1062 Some("Debug Panel")
1063 } else {
1064 None
1065 }
1066 }
1067
1068 fn toggle_action(&self) -> Box<dyn Action> {
1069 Box::new(ToggleFocus)
1070 }
1071
1072 fn activation_priority(&self) -> u32 {
1073 9
1074 }
1075 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1076}
1077
1078impl Render for DebugPanel {
1079 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1080 let has_sessions = self.sessions.len() > 0;
1081 debug_assert_eq!(has_sessions, self.active_session.is_some());
1082
1083 if self
1084 .active_session
1085 .as_ref()
1086 .and_then(|session| session.read(cx).mode().as_running().cloned())
1087 .map(|state| state.read(cx).has_open_context_menu(cx))
1088 .unwrap_or(false)
1089 {
1090 self.context_menu.take();
1091 }
1092
1093 v_flex()
1094 .size_full()
1095 .key_context("DebugPanel")
1096 .child(h_flex().children(self.top_controls_strip(window, cx)))
1097 .track_focus(&self.focus_handle(cx))
1098 .when(self.active_session.is_some(), |this| {
1099 this.on_mouse_down(
1100 MouseButton::Right,
1101 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1102 if this
1103 .active_session
1104 .as_ref()
1105 .and_then(|session| {
1106 session.read(cx).mode().as_running().map(|state| {
1107 state.read(cx).has_pane_at_position(event.position)
1108 })
1109 })
1110 .unwrap_or(false)
1111 {
1112 this.deploy_context_menu(event.position, window, cx);
1113 }
1114 }),
1115 )
1116 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1117 deferred(
1118 anchored()
1119 .position(*position)
1120 .anchor(gpui::Corner::TopLeft)
1121 .child(menu.clone()),
1122 )
1123 .with_priority(1)
1124 }))
1125 })
1126 .map(|this| {
1127 if has_sessions {
1128 this.children(self.active_session.clone())
1129 } else {
1130 this.child(
1131 v_flex()
1132 .h_full()
1133 .gap_1()
1134 .items_center()
1135 .justify_center()
1136 .child(
1137 h_flex().child(
1138 Label::new("No Debugging Sessions")
1139 .size(LabelSize::Small)
1140 .color(Color::Muted),
1141 ),
1142 )
1143 .child(
1144 h_flex().flex_shrink().child(
1145 Button::new("spawn-new-session-empty-state", "New Session")
1146 .size(ButtonSize::Large)
1147 .on_click(|_, window, cx| {
1148 window.dispatch_action(
1149 CreateDebuggingSession.boxed_clone(),
1150 cx,
1151 );
1152 }),
1153 ),
1154 ),
1155 )
1156 }
1157 })
1158 .into_any()
1159 }
1160}
1161
1162struct DebuggerProvider(Entity<DebugPanel>);
1163
1164impl workspace::DebuggerProvider for DebuggerProvider {
1165 fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
1166 self.0.update(cx, |_, cx| {
1167 cx.defer_in(window, |this, window, cx| {
1168 this.start_session(definition, window, cx);
1169 })
1170 })
1171 }
1172}