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