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