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