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