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, 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 breakpoint_store = self.project.read(cx).breakpoint_store();
357 let definition = parent_session.read(cx).definition().clone();
358 let mut binary = parent_session.read(cx).binary().clone();
359 binary.request_args = request.clone();
360
361 cx.spawn_in(window, async move |this, cx| {
362 let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
363 let session =
364 dap_store.new_session(definition.clone(), Some(parent_session.clone()), cx);
365
366 let task = session.update(cx, |session, cx| {
367 session.boot(
368 binary,
369 worktree,
370 breakpoint_store,
371 dap_store_handle.downgrade(),
372 cx,
373 )
374 });
375 (session, task)
376 })?;
377
378 match task.await {
379 Err(e) => {
380 this.update(cx, |this, cx| {
381 this.workspace
382 .update(cx, |workspace, cx| {
383 workspace.show_error(&e, cx);
384 })
385 .ok();
386 })
387 .ok();
388
389 session
390 .update(cx, |session, cx| session.shutdown(cx))?
391 .await;
392 }
393 Ok(_) => Self::register_session(this, session, cx).await?,
394 }
395
396 anyhow::Ok(())
397 })
398 .detach_and_log_err(cx);
399 }
400
401 pub fn active_session(&self) -> Option<Entity<DebugSession>> {
402 self.active_session.clone()
403 }
404
405 pub fn resolve_scenario(
406 &self,
407 scenario: DebugScenario,
408 task_context: TaskContext,
409 buffer: Option<Entity<Buffer>>,
410 window: &Window,
411 cx: &mut Context<Self>,
412 ) -> Task<Result<DebugTaskDefinition>> {
413 let project = self.project.read(cx);
414 let dap_store = project.dap_store().downgrade();
415 let task_store = project.task_store().downgrade();
416 let workspace = self.workspace.clone();
417 cx.spawn_in(window, async move |_, cx| {
418 let DebugScenario {
419 adapter,
420 label,
421 build,
422 request,
423 initialize_args,
424 tcp_connection,
425 stop_on_entry,
426 } = scenario;
427 let request = if let Some(mut request) = request {
428 if let DebugRequest::Launch(launch_config) = &mut request {
429 let mut variable_names = HashMap::default();
430 let mut substituted_variables = HashSet::default();
431 let task_variables = task_context
432 .task_variables
433 .iter()
434 .map(|(key, value)| {
435 let key_string = key.to_string();
436 if !variable_names.contains_key(&key_string) {
437 variable_names.insert(key_string.clone(), key.clone());
438 }
439 (key_string, value.as_str())
440 })
441 .collect::<HashMap<_, _>>();
442
443 let cwd = launch_config
444 .cwd
445 .as_ref()
446 .and_then(|cwd| cwd.to_str())
447 .and_then(|cwd| {
448 task::substitute_all_template_variables_in_str(
449 cwd,
450 &task_variables,
451 &variable_names,
452 &mut substituted_variables,
453 )
454 });
455
456 if let Some(cwd) = cwd {
457 launch_config.cwd = Some(PathBuf::from(cwd))
458 }
459
460 if let Some(program) = task::substitute_all_template_variables_in_str(
461 &launch_config.program,
462 &task_variables,
463 &variable_names,
464 &mut substituted_variables,
465 ) {
466 launch_config.program = program;
467 }
468
469 for arg in launch_config.args.iter_mut() {
470 if let Some(substituted_arg) =
471 task::substitute_all_template_variables_in_str(
472 &arg,
473 &task_variables,
474 &variable_names,
475 &mut substituted_variables,
476 )
477 {
478 *arg = substituted_arg;
479 }
480 }
481 }
482
483 request
484 } else if let Some(build) = build {
485 let Some(task) = task_store.update(cx, |this, cx| {
486 this.task_inventory().and_then(|inventory| {
487 inventory
488 .read(cx)
489 .task_template_by_label(buffer, &build, cx)
490 })
491 })?
492 else {
493 anyhow::bail!("Couldn't find task template for {:?}", build)
494 };
495 let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
496 anyhow::bail!("Could not resolve task variables within a debug scenario");
497 };
498
499 let run_build = workspace.update_in(cx, |workspace, window, cx| {
500 workspace.spawn_in_terminal(task.resolved.clone(), window, cx)
501 })?;
502
503 let exit_status = run_build.await?;
504 if !exit_status.success() {
505 anyhow::bail!("Build failed");
506 }
507
508 dap_store
509 .update(cx, |this, cx| this.run_debug_locator(task.resolved, cx))?
510 .await?
511 } else {
512 return Err(anyhow!("No request or build provided"));
513 };
514 Ok(DebugTaskDefinition {
515 label,
516 adapter,
517 request,
518 initialize_args,
519 stop_on_entry,
520 tcp_connection,
521 })
522 })
523 }
524
525 fn close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
526 let Some(session) = self
527 .sessions
528 .iter()
529 .find(|other| entity_id == other.entity_id())
530 .cloned()
531 else {
532 return;
533 };
534 session.update(cx, |this, cx| {
535 this.running_state().update(cx, |this, cx| {
536 this.serialize_layout(window, cx);
537 });
538 });
539 let session_id = session.update(cx, |this, cx| this.session_id(cx));
540 let should_prompt = self
541 .project
542 .update(cx, |this, cx| {
543 let session = this.dap_store().read(cx).session_by_id(session_id);
544 session.map(|session| !session.read(cx).is_terminated())
545 })
546 .unwrap_or_default();
547
548 cx.spawn_in(window, async move |this, cx| {
549 if should_prompt {
550 let response = cx.prompt(
551 gpui::PromptLevel::Warning,
552 "This Debug Session is still running. Are you sure you want to terminate it?",
553 None,
554 &["Yes", "No"],
555 );
556 if response.await == Ok(1) {
557 return;
558 }
559 }
560 session.update(cx, |session, cx| session.shutdown(cx)).ok();
561 this.update(cx, |this, cx| {
562 this.sessions.retain(|other| entity_id != other.entity_id());
563
564 if let Some(active_session_id) = this
565 .active_session
566 .as_ref()
567 .map(|session| session.entity_id())
568 {
569 if active_session_id == entity_id {
570 this.active_session = this.sessions.first().cloned();
571 }
572 }
573 cx.notify()
574 })
575 .ok();
576 })
577 .detach();
578 }
579 fn sessions_drop_down_menu(
580 &self,
581 active_session: &Entity<DebugSession>,
582 window: &mut Window,
583 cx: &mut Context<Self>,
584 ) -> DropdownMenu {
585 let sessions = self.sessions.clone();
586 let weak = cx.weak_entity();
587 let label = active_session.read(cx).label_element(cx);
588
589 DropdownMenu::new_with_element(
590 "debugger-session-list",
591 label,
592 ContextMenu::build(window, cx, move |mut this, _, cx| {
593 let context_menu = cx.weak_entity();
594 for session in sessions.into_iter() {
595 let weak_session = session.downgrade();
596 let weak_session_id = weak_session.entity_id();
597
598 this = this.custom_entry(
599 {
600 let weak = weak.clone();
601 let context_menu = context_menu.clone();
602 move |_, cx| {
603 weak_session
604 .read_with(cx, |session, cx| {
605 let context_menu = context_menu.clone();
606 let id: SharedString =
607 format!("debug-session-{}", session.session_id(cx).0)
608 .into();
609 h_flex()
610 .w_full()
611 .group(id.clone())
612 .justify_between()
613 .child(session.label_element(cx))
614 .child(
615 IconButton::new(
616 "close-debug-session",
617 IconName::Close,
618 )
619 .visible_on_hover(id.clone())
620 .icon_size(IconSize::Small)
621 .on_click({
622 let weak = weak.clone();
623 move |_, window, cx| {
624 weak.update(cx, |panel, cx| {
625 panel.close_session(
626 weak_session_id,
627 window,
628 cx,
629 );
630 })
631 .ok();
632 context_menu
633 .update(cx, |this, cx| {
634 this.cancel(
635 &Default::default(),
636 window,
637 cx,
638 );
639 })
640 .ok();
641 }
642 }),
643 )
644 .into_any_element()
645 })
646 .unwrap_or_else(|_| div().into_any_element())
647 }
648 },
649 {
650 let weak = weak.clone();
651 move |window, cx| {
652 weak.update(cx, |panel, cx| {
653 panel.activate_session(session.clone(), window, cx);
654 })
655 .ok();
656 }
657 },
658 );
659 }
660 this
661 }),
662 )
663 }
664
665 fn deploy_context_menu(
666 &mut self,
667 position: Point<Pixels>,
668 window: &mut Window,
669 cx: &mut Context<Self>,
670 ) {
671 if let Some(running_state) = self
672 .active_session
673 .as_ref()
674 .map(|session| session.read(cx).running_state().clone())
675 {
676 let pane_items_status = running_state.read(cx).pane_items_status(cx);
677 let this = cx.weak_entity();
678
679 let context_menu = ContextMenu::build(window, cx, |mut menu, _window, _cx| {
680 for (item_kind, is_visible) in pane_items_status.into_iter() {
681 menu = menu.toggleable_entry(item_kind, is_visible, IconPosition::End, None, {
682 let this = this.clone();
683 move |window, cx| {
684 this.update(cx, |this, cx| {
685 if let Some(running_state) = this
686 .active_session
687 .as_ref()
688 .map(|session| session.read(cx).running_state().clone())
689 {
690 running_state.update(cx, |state, cx| {
691 if is_visible {
692 state.remove_pane_item(item_kind, window, cx);
693 } else {
694 state.add_pane_item(item_kind, position, window, cx);
695 }
696 })
697 }
698 })
699 .ok();
700 }
701 });
702 }
703
704 menu
705 });
706
707 window.focus(&context_menu.focus_handle(cx));
708 let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
709 this.context_menu.take();
710 cx.notify();
711 });
712 self.context_menu = Some((context_menu, position, subscription));
713 }
714 }
715
716 fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
717 let active_session = self.active_session.clone();
718 let focus_handle = self.focus_handle.clone();
719
720 Some(
721 h_flex()
722 .border_b_1()
723 .border_color(cx.theme().colors().border)
724 .p_1()
725 .justify_between()
726 .w_full()
727 .child(
728 h_flex().gap_2().w_full().when_some(
729 active_session
730 .as_ref()
731 .map(|session| session.read(cx).running_state()),
732 |this, running_session| {
733 let thread_status = running_session
734 .read(cx)
735 .thread_status(cx)
736 .unwrap_or(project::debugger::session::ThreadStatus::Exited);
737 let capabilities = running_session.read(cx).capabilities(cx);
738 this.map(|this| {
739 if thread_status == ThreadStatus::Running {
740 this.child(
741 IconButton::new("debug-pause", IconName::DebugPause)
742 .icon_size(IconSize::XSmall)
743 .shape(ui::IconButtonShape::Square)
744 .on_click(window.listener_for(
745 &running_session,
746 |this, _, _window, cx| {
747 this.pause_thread(cx);
748 },
749 ))
750 .tooltip({
751 let focus_handle = focus_handle.clone();
752 move |window, cx| {
753 Tooltip::for_action_in(
754 "Pause program",
755 &Pause,
756 &focus_handle,
757 window,
758 cx,
759 )
760 }
761 }),
762 )
763 } else {
764 this.child(
765 IconButton::new("debug-continue", IconName::DebugContinue)
766 .icon_size(IconSize::XSmall)
767 .shape(ui::IconButtonShape::Square)
768 .on_click(window.listener_for(
769 &running_session,
770 |this, _, _window, cx| this.continue_thread(cx),
771 ))
772 .disabled(thread_status != ThreadStatus::Stopped)
773 .tooltip({
774 let focus_handle = focus_handle.clone();
775 move |window, cx| {
776 Tooltip::for_action_in(
777 "Continue program",
778 &Continue,
779 &focus_handle,
780 window,
781 cx,
782 )
783 }
784 }),
785 )
786 }
787 })
788 .child(
789 IconButton::new("debug-step-over", IconName::ArrowRight)
790 .icon_size(IconSize::XSmall)
791 .shape(ui::IconButtonShape::Square)
792 .on_click(window.listener_for(
793 &running_session,
794 |this, _, _window, cx| {
795 this.step_over(cx);
796 },
797 ))
798 .disabled(thread_status != ThreadStatus::Stopped)
799 .tooltip({
800 let focus_handle = focus_handle.clone();
801 move |window, cx| {
802 Tooltip::for_action_in(
803 "Step over",
804 &StepOver,
805 &focus_handle,
806 window,
807 cx,
808 )
809 }
810 }),
811 )
812 .child(
813 IconButton::new("debug-step-out", IconName::ArrowUpRight)
814 .icon_size(IconSize::XSmall)
815 .shape(ui::IconButtonShape::Square)
816 .on_click(window.listener_for(
817 &running_session,
818 |this, _, _window, cx| {
819 this.step_out(cx);
820 },
821 ))
822 .disabled(thread_status != ThreadStatus::Stopped)
823 .tooltip({
824 let focus_handle = focus_handle.clone();
825 move |window, cx| {
826 Tooltip::for_action_in(
827 "Step out",
828 &StepOut,
829 &focus_handle,
830 window,
831 cx,
832 )
833 }
834 }),
835 )
836 .child(
837 IconButton::new("debug-step-into", IconName::ArrowDownRight)
838 .icon_size(IconSize::XSmall)
839 .shape(ui::IconButtonShape::Square)
840 .on_click(window.listener_for(
841 &running_session,
842 |this, _, _window, cx| {
843 this.step_in(cx);
844 },
845 ))
846 .disabled(thread_status != ThreadStatus::Stopped)
847 .tooltip({
848 let focus_handle = focus_handle.clone();
849 move |window, cx| {
850 Tooltip::for_action_in(
851 "Step in",
852 &StepInto,
853 &focus_handle,
854 window,
855 cx,
856 )
857 }
858 }),
859 )
860 .child(Divider::vertical())
861 .child(
862 IconButton::new(
863 "debug-enable-breakpoint",
864 IconName::DebugDisabledBreakpoint,
865 )
866 .icon_size(IconSize::XSmall)
867 .shape(ui::IconButtonShape::Square)
868 .disabled(thread_status != ThreadStatus::Stopped),
869 )
870 .child(
871 IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
872 .icon_size(IconSize::XSmall)
873 .shape(ui::IconButtonShape::Square)
874 .disabled(thread_status != ThreadStatus::Stopped),
875 )
876 .child(
877 IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
878 .icon_size(IconSize::XSmall)
879 .shape(ui::IconButtonShape::Square)
880 .disabled(
881 thread_status == ThreadStatus::Exited
882 || thread_status == ThreadStatus::Ended,
883 )
884 .on_click(window.listener_for(
885 &running_session,
886 |this, _, _window, cx| {
887 this.toggle_ignore_breakpoints(cx);
888 },
889 ))
890 .tooltip({
891 let focus_handle = focus_handle.clone();
892 move |window, cx| {
893 Tooltip::for_action_in(
894 "Disable all breakpoints",
895 &ToggleIgnoreBreakpoints,
896 &focus_handle,
897 window,
898 cx,
899 )
900 }
901 }),
902 )
903 .child(Divider::vertical())
904 .child(
905 IconButton::new("debug-restart", IconName::DebugRestart)
906 .icon_size(IconSize::XSmall)
907 .on_click(window.listener_for(
908 &running_session,
909 |this, _, _window, cx| {
910 this.restart_session(cx);
911 },
912 ))
913 .tooltip({
914 let focus_handle = focus_handle.clone();
915 move |window, cx| {
916 Tooltip::for_action_in(
917 "Restart",
918 &Restart,
919 &focus_handle,
920 window,
921 cx,
922 )
923 }
924 }),
925 )
926 .child(
927 IconButton::new("debug-stop", IconName::Power)
928 .icon_size(IconSize::XSmall)
929 .on_click(window.listener_for(
930 &running_session,
931 |this, _, _window, cx| {
932 this.stop_thread(cx);
933 },
934 ))
935 .disabled(
936 thread_status != ThreadStatus::Stopped
937 && thread_status != ThreadStatus::Running,
938 )
939 .tooltip({
940 let focus_handle = focus_handle.clone();
941 let label = if capabilities
942 .supports_terminate_threads_request
943 .unwrap_or_default()
944 {
945 "Terminate Thread"
946 } else {
947 "Terminate All Threads"
948 };
949 move |window, cx| {
950 Tooltip::for_action_in(
951 label,
952 &Stop,
953 &focus_handle,
954 window,
955 cx,
956 )
957 }
958 }),
959 )
960 },
961 ),
962 )
963 .child(
964 h_flex()
965 .gap_2()
966 .when_some(
967 active_session
968 .as_ref()
969 .map(|session| session.read(cx).running_state())
970 .cloned(),
971 |this, session| {
972 this.child(
973 session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
974 )
975 .child(Divider::vertical())
976 },
977 )
978 .when_some(active_session.as_ref(), |this, session| {
979 let context_menu = self.sessions_drop_down_menu(session, window, cx);
980 this.child(context_menu).child(Divider::vertical())
981 })
982 .child(
983 IconButton::new("debug-new-session", IconName::Plus)
984 .icon_size(IconSize::Small)
985 .on_click({
986 let workspace = self.workspace.clone();
987 let weak_panel = cx.weak_entity();
988 let past_debug_definition = self.past_debug_definition.clone();
989 move |_, window, cx| {
990 let weak_panel = weak_panel.clone();
991 let past_debug_definition = past_debug_definition.clone();
992
993 let _ = workspace.update(cx, |this, cx| {
994 let workspace = cx.weak_entity();
995 this.toggle_modal(window, cx, |window, cx| {
996 NewSessionModal::new(
997 past_debug_definition,
998 weak_panel,
999 workspace,
1000 None,
1001 window,
1002 cx,
1003 )
1004 });
1005 });
1006 }
1007 })
1008 .tooltip({
1009 let focus_handle = focus_handle.clone();
1010 move |window, cx| {
1011 Tooltip::for_action_in(
1012 "New Debug Session",
1013 &CreateDebuggingSession,
1014 &focus_handle,
1015 window,
1016 cx,
1017 )
1018 }
1019 }),
1020 ),
1021 ),
1022 )
1023 }
1024
1025 fn activate_pane_in_direction(
1026 &mut self,
1027 direction: SplitDirection,
1028 window: &mut Window,
1029 cx: &mut Context<Self>,
1030 ) {
1031 if let Some(session) = self.active_session() {
1032 session.update(cx, |session, cx| {
1033 session.running_state().update(cx, |running, cx| {
1034 running.activate_pane_in_direction(direction, window, cx);
1035 })
1036 });
1037 }
1038 }
1039
1040 fn activate_item(
1041 &mut self,
1042 item: DebuggerPaneItem,
1043 window: &mut Window,
1044 cx: &mut Context<Self>,
1045 ) {
1046 if let Some(session) = self.active_session() {
1047 session.update(cx, |session, cx| {
1048 session.running_state().update(cx, |running, cx| {
1049 running.activate_item(item, window, cx);
1050 });
1051 });
1052 }
1053 }
1054
1055 fn activate_session(
1056 &mut self,
1057 session_item: Entity<DebugSession>,
1058 window: &mut Window,
1059 cx: &mut Context<Self>,
1060 ) {
1061 debug_assert!(self.sessions.contains(&session_item));
1062 session_item.focus_handle(cx).focus(window);
1063 session_item.update(cx, |this, cx| {
1064 this.running_state().update(cx, |this, cx| {
1065 this.go_to_selected_stack_frame(window, cx);
1066 });
1067 });
1068 self.active_session = Some(session_item);
1069 cx.notify();
1070 }
1071}
1072
1073impl EventEmitter<PanelEvent> for DebugPanel {}
1074impl EventEmitter<DebugPanelEvent> for DebugPanel {}
1075
1076impl Focusable for DebugPanel {
1077 fn focus_handle(&self, _: &App) -> FocusHandle {
1078 self.focus_handle.clone()
1079 }
1080}
1081
1082impl Panel for DebugPanel {
1083 fn persistent_name() -> &'static str {
1084 "DebugPanel"
1085 }
1086
1087 fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
1088 DockPosition::Bottom
1089 }
1090
1091 fn position_is_valid(&self, position: DockPosition) -> bool {
1092 position == DockPosition::Bottom
1093 }
1094
1095 fn set_position(
1096 &mut self,
1097 _position: DockPosition,
1098 _window: &mut Window,
1099 _cx: &mut Context<Self>,
1100 ) {
1101 }
1102
1103 fn size(&self, _window: &Window, _: &App) -> Pixels {
1104 self.size
1105 }
1106
1107 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
1108 self.size = size.unwrap();
1109 }
1110
1111 fn remote_id() -> Option<proto::PanelId> {
1112 Some(proto::PanelId::DebugPanel)
1113 }
1114
1115 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
1116 Some(IconName::Debug)
1117 }
1118
1119 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
1120 if DebuggerSettings::get_global(cx).button {
1121 Some("Debug Panel")
1122 } else {
1123 None
1124 }
1125 }
1126
1127 fn toggle_action(&self) -> Box<dyn Action> {
1128 Box::new(ToggleFocus)
1129 }
1130
1131 fn pane(&self) -> Option<Entity<Pane>> {
1132 None
1133 }
1134
1135 fn activation_priority(&self) -> u32 {
1136 9
1137 }
1138
1139 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
1140}
1141
1142impl Render for DebugPanel {
1143 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1144 let has_sessions = self.sessions.len() > 0;
1145 let this = cx.weak_entity();
1146 debug_assert_eq!(has_sessions, self.active_session.is_some());
1147
1148 if self
1149 .active_session
1150 .as_ref()
1151 .map(|session| session.read(cx).running_state())
1152 .map(|state| state.read(cx).has_open_context_menu(cx))
1153 .unwrap_or(false)
1154 {
1155 self.context_menu.take();
1156 }
1157
1158 v_flex()
1159 .size_full()
1160 .key_context("DebugPanel")
1161 .child(h_flex().children(self.top_controls_strip(window, cx)))
1162 .track_focus(&self.focus_handle(cx))
1163 .on_action({
1164 let this = this.clone();
1165 move |_: &workspace::ActivatePaneLeft, window, cx| {
1166 this.update(cx, |this, cx| {
1167 this.activate_pane_in_direction(SplitDirection::Left, window, cx);
1168 })
1169 .ok();
1170 }
1171 })
1172 .on_action({
1173 let this = this.clone();
1174 move |_: &workspace::ActivatePaneRight, window, cx| {
1175 this.update(cx, |this, cx| {
1176 this.activate_pane_in_direction(SplitDirection::Right, window, cx);
1177 })
1178 .ok();
1179 }
1180 })
1181 .on_action({
1182 let this = this.clone();
1183 move |_: &workspace::ActivatePaneUp, window, cx| {
1184 this.update(cx, |this, cx| {
1185 this.activate_pane_in_direction(SplitDirection::Up, window, cx);
1186 })
1187 .ok();
1188 }
1189 })
1190 .on_action({
1191 let this = this.clone();
1192 move |_: &workspace::ActivatePaneDown, window, cx| {
1193 this.update(cx, |this, cx| {
1194 this.activate_pane_in_direction(SplitDirection::Down, window, cx);
1195 })
1196 .ok();
1197 }
1198 })
1199 .on_action({
1200 let this = this.clone();
1201 move |_: &FocusConsole, window, cx| {
1202 this.update(cx, |this, cx| {
1203 this.activate_item(DebuggerPaneItem::Console, window, cx);
1204 })
1205 .ok();
1206 }
1207 })
1208 .on_action({
1209 let this = this.clone();
1210 move |_: &FocusVariables, window, cx| {
1211 this.update(cx, |this, cx| {
1212 this.activate_item(DebuggerPaneItem::Variables, window, cx);
1213 })
1214 .ok();
1215 }
1216 })
1217 .on_action({
1218 let this = this.clone();
1219 move |_: &FocusBreakpointList, window, cx| {
1220 this.update(cx, |this, cx| {
1221 this.activate_item(DebuggerPaneItem::BreakpointList, window, cx);
1222 })
1223 .ok();
1224 }
1225 })
1226 .on_action({
1227 let this = this.clone();
1228 move |_: &FocusFrames, window, cx| {
1229 this.update(cx, |this, cx| {
1230 this.activate_item(DebuggerPaneItem::Frames, window, cx);
1231 })
1232 .ok();
1233 }
1234 })
1235 .on_action({
1236 let this = this.clone();
1237 move |_: &FocusModules, window, cx| {
1238 this.update(cx, |this, cx| {
1239 this.activate_item(DebuggerPaneItem::Modules, window, cx);
1240 })
1241 .ok();
1242 }
1243 })
1244 .on_action({
1245 let this = this.clone();
1246 move |_: &FocusLoadedSources, window, cx| {
1247 this.update(cx, |this, cx| {
1248 this.activate_item(DebuggerPaneItem::LoadedSources, window, cx);
1249 })
1250 .ok();
1251 }
1252 })
1253 .on_action({
1254 let this = this.clone();
1255 move |_: &FocusTerminal, window, cx| {
1256 this.update(cx, |this, cx| {
1257 this.activate_item(DebuggerPaneItem::Terminal, window, cx);
1258 })
1259 .ok();
1260 }
1261 })
1262 .when(self.active_session.is_some(), |this| {
1263 this.on_mouse_down(
1264 MouseButton::Right,
1265 cx.listener(|this, event: &MouseDownEvent, window, cx| {
1266 if this
1267 .active_session
1268 .as_ref()
1269 .map(|session| {
1270 let state = session.read(cx).running_state();
1271 state.read(cx).has_pane_at_position(event.position)
1272 })
1273 .unwrap_or(false)
1274 {
1275 this.deploy_context_menu(event.position, window, cx);
1276 }
1277 }),
1278 )
1279 .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1280 deferred(
1281 anchored()
1282 .position(*position)
1283 .anchor(gpui::Corner::TopLeft)
1284 .child(menu.clone()),
1285 )
1286 .with_priority(1)
1287 }))
1288 })
1289 .map(|this| {
1290 if has_sessions {
1291 this.children(self.active_session.clone())
1292 } else {
1293 this.child(
1294 v_flex()
1295 .h_full()
1296 .gap_1()
1297 .items_center()
1298 .justify_center()
1299 .child(
1300 h_flex().child(
1301 Label::new("No Debugging Sessions")
1302 .size(LabelSize::Small)
1303 .color(Color::Muted),
1304 ),
1305 )
1306 .child(
1307 h_flex().flex_shrink().child(
1308 Button::new("spawn-new-session-empty-state", "New Session")
1309 .size(ButtonSize::Large)
1310 .on_click(|_, window, cx| {
1311 window.dispatch_action(
1312 CreateDebuggingSession.boxed_clone(),
1313 cx,
1314 );
1315 }),
1316 ),
1317 ),
1318 )
1319 }
1320 })
1321 .into_any()
1322 }
1323}
1324
1325struct DebuggerProvider(Entity<DebugPanel>);
1326
1327impl workspace::DebuggerProvider for DebuggerProvider {
1328 fn start_session(
1329 &self,
1330 definition: DebugScenario,
1331 context: TaskContext,
1332 buffer: Option<Entity<Buffer>>,
1333 window: &mut Window,
1334 cx: &mut App,
1335 ) {
1336 self.0.update(cx, |_, cx| {
1337 cx.defer_in(window, |this, window, cx| {
1338 this.start_session(definition, context, buffer, window, cx);
1339 })
1340 })
1341 }
1342}