debugger_panel.rs

  1use crate::{
  2    ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
  3    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
  4};
  5use crate::{new_session_modal::NewSessionModal, session::DebugSession};
  6use anyhow::{Result, anyhow};
  7use collections::HashMap;
  8use command_palette_hooks::CommandPaletteFilter;
  9use dap::{
 10    ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
 11    client::SessionId, debugger_settings::DebuggerSettings,
 12};
 13use futures::{SinkExt as _, channel::mpsc};
 14use gpui::{
 15    Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
 16    Subscription, Task, WeakEntity, actions,
 17};
 18use project::{
 19    Project,
 20    debugger::{
 21        dap_store::{self, DapStore},
 22        session::ThreadStatus,
 23    },
 24    terminals::TerminalKind,
 25};
 26use rpc::proto::{self};
 27use settings::Settings;
 28use std::{any::TypeId, path::PathBuf};
 29use task::DebugTaskDefinition;
 30use terminal_view::terminal_panel::TerminalPanel;
 31use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 32use workspace::{
 33    Workspace,
 34    dock::{DockPosition, Panel, PanelEvent},
 35};
 36
 37pub enum DebugPanelEvent {
 38    Exited(SessionId),
 39    Terminated(SessionId),
 40    Stopped {
 41        client_id: SessionId,
 42        event: StoppedEvent,
 43        go_to_stack_frame: bool,
 44    },
 45    Thread((SessionId, ThreadEvent)),
 46    Continued((SessionId, ContinuedEvent)),
 47    Output((SessionId, OutputEvent)),
 48    Module((SessionId, ModuleEvent)),
 49    LoadedSource((SessionId, LoadedSourceEvent)),
 50    ClientShutdown(SessionId),
 51    CapabilitiesChanged(SessionId),
 52}
 53
 54actions!(debug_panel, [ToggleFocus]);
 55pub struct DebugPanel {
 56    size: Pixels,
 57    sessions: Vec<Entity<DebugSession>>,
 58    active_session: Option<Entity<DebugSession>>,
 59    /// This represents the last debug definition that was created in the new session modal
 60    pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
 61    project: WeakEntity<Project>,
 62    workspace: WeakEntity<Workspace>,
 63    focus_handle: FocusHandle,
 64    _subscriptions: Vec<Subscription>,
 65}
 66
 67impl DebugPanel {
 68    pub fn new(
 69        workspace: &Workspace,
 70        window: &mut Window,
 71        cx: &mut Context<Workspace>,
 72    ) -> Entity<Self> {
 73        cx.new(|cx| {
 74            let project = workspace.project().clone();
 75            let dap_store = project.read(cx).dap_store();
 76
 77            let _subscriptions =
 78                vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
 79
 80            let debug_panel = Self {
 81                size: px(300.),
 82                sessions: vec![],
 83                active_session: None,
 84                _subscriptions,
 85                past_debug_definition: None,
 86                focus_handle: cx.focus_handle(),
 87                project: project.downgrade(),
 88                workspace: workspace.weak_handle(),
 89            };
 90
 91            debug_panel
 92        })
 93    }
 94
 95    pub fn load(
 96        workspace: WeakEntity<Workspace>,
 97        cx: AsyncWindowContext,
 98    ) -> Task<Result<Entity<Self>>> {
 99        cx.spawn(async move |cx| {
100            workspace.update_in(cx, |workspace, window, cx| {
101                let debug_panel = DebugPanel::new(workspace, window, cx);
102
103                workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
104                    workspace.project().read(cx).breakpoint_store().update(
105                        cx,
106                        |breakpoint_store, cx| {
107                            breakpoint_store.clear_breakpoints(cx);
108                        },
109                    )
110                });
111
112                cx.observe(&debug_panel, |_, debug_panel, cx| {
113                    let (has_active_session, supports_restart, support_step_back) = debug_panel
114                        .update(cx, |this, cx| {
115                            this.active_session()
116                                .map(|item| {
117                                    let running = item.read(cx).mode().as_running().cloned();
118
119                                    match running {
120                                        Some(running) => {
121                                            let caps = running.read(cx).capabilities(cx);
122                                            (
123                                                true,
124                                                caps.supports_restart_request.unwrap_or_default(),
125                                                caps.supports_step_back.unwrap_or_default(),
126                                            )
127                                        }
128                                        None => (false, false, false),
129                                    }
130                                })
131                                .unwrap_or((false, false, false))
132                        });
133
134                    let filter = CommandPaletteFilter::global_mut(cx);
135                    let debugger_action_types = [
136                        TypeId::of::<Continue>(),
137                        TypeId::of::<StepOver>(),
138                        TypeId::of::<StepInto>(),
139                        TypeId::of::<StepOut>(),
140                        TypeId::of::<Stop>(),
141                        TypeId::of::<Disconnect>(),
142                        TypeId::of::<Pause>(),
143                        TypeId::of::<ToggleIgnoreBreakpoints>(),
144                    ];
145
146                    let step_back_action_type = [TypeId::of::<StepBack>()];
147                    let restart_action_type = [TypeId::of::<Restart>()];
148
149                    if has_active_session {
150                        filter.show_action_types(debugger_action_types.iter());
151
152                        if supports_restart {
153                            filter.show_action_types(restart_action_type.iter());
154                        } else {
155                            filter.hide_action_types(&restart_action_type);
156                        }
157
158                        if support_step_back {
159                            filter.show_action_types(step_back_action_type.iter());
160                        } else {
161                            filter.hide_action_types(&step_back_action_type);
162                        }
163                    } else {
164                        // show only the `debug: start`
165                        filter.hide_action_types(&debugger_action_types);
166                        filter.hide_action_types(&step_back_action_type);
167                        filter.hide_action_types(&restart_action_type);
168                    }
169                })
170                .detach();
171
172                debug_panel
173            })
174        })
175    }
176
177    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
178        self.active_session.clone()
179    }
180
181    pub fn debug_panel_items_by_client(
182        &self,
183        client_id: &SessionId,
184        cx: &Context<Self>,
185    ) -> Vec<Entity<DebugSession>> {
186        self.sessions
187            .iter()
188            .filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
189            .map(|item| item.clone())
190            .collect()
191    }
192
193    pub fn debug_panel_item_by_client(
194        &self,
195        client_id: SessionId,
196        cx: &mut Context<Self>,
197    ) -> Option<Entity<DebugSession>> {
198        self.sessions
199            .iter()
200            .find(|item| {
201                let item = item.read(cx);
202
203                item.session_id(cx) == Some(client_id)
204            })
205            .cloned()
206    }
207
208    fn handle_dap_store_event(
209        &mut self,
210        dap_store: &Entity<DapStore>,
211        event: &dap_store::DapStoreEvent,
212        window: &mut Window,
213        cx: &mut Context<Self>,
214    ) {
215        match event {
216            dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
217                let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
218                    return log::error!(
219                        "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
220                    );
221                };
222
223                let Some(project) = self.project.upgrade() else {
224                    return log::error!("Debug Panel out lived it's weak reference to Project");
225                };
226
227                if self
228                    .sessions
229                    .iter()
230                    .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
231                {
232                    // We already have an item for this session.
233                    return;
234                }
235                let session_item = DebugSession::running(
236                    project,
237                    self.workspace.clone(),
238                    session,
239                    cx.weak_entity(),
240                    window,
241                    cx,
242                );
243
244                self.sessions.push(session_item.clone());
245                self.activate_session(session_item, window, cx);
246            }
247            dap_store::DapStoreEvent::RunInTerminal {
248                title,
249                cwd,
250                command,
251                args,
252                envs,
253                sender,
254                ..
255            } => {
256                self.handle_run_in_terminal_request(
257                    title.clone(),
258                    cwd.clone(),
259                    command.clone(),
260                    args.clone(),
261                    envs.clone(),
262                    sender.clone(),
263                    window,
264                    cx,
265                )
266                .detach_and_log_err(cx);
267            }
268            _ => {}
269        }
270    }
271
272    fn handle_run_in_terminal_request(
273        &self,
274        title: Option<String>,
275        cwd: PathBuf,
276        command: Option<String>,
277        args: Vec<String>,
278        envs: HashMap<String, String>,
279        mut sender: mpsc::Sender<Result<u32>>,
280        window: &mut Window,
281        cx: &mut App,
282    ) -> Task<Result<()>> {
283        let terminal_task = self.workspace.update(cx, |workspace, cx| {
284            let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
285                anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
286            });
287
288            let terminal_panel = match terminal_panel {
289                Ok(panel) => panel,
290                Err(err) => return Task::ready(Err(err)),
291            };
292
293            terminal_panel.update(cx, |terminal_panel, cx| {
294                let terminal_task = terminal_panel.add_terminal(
295                    TerminalKind::Debug {
296                        command,
297                        args,
298                        envs,
299                        cwd,
300                        title,
301                    },
302                    task::RevealStrategy::Always,
303                    window,
304                    cx,
305                );
306
307                cx.spawn(async move |_, cx| {
308                    let pid_task = async move {
309                        let terminal = terminal_task.await?;
310
311                        terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
312                    };
313
314                    pid_task.await
315                })
316            })
317        });
318
319        cx.background_spawn(async move {
320            match terminal_task {
321                Ok(pid_task) => match pid_task.await {
322                    Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
323                    Ok(None) => {
324                        sender
325                            .send(Err(anyhow!(
326                                "Terminal was spawned but PID was not available"
327                            )))
328                            .await?
329                    }
330                    Err(error) => sender.send(Err(anyhow!(error))).await?,
331                },
332                Err(error) => sender.send(Err(anyhow!(error))).await?,
333            };
334
335            Ok(())
336        })
337    }
338
339    fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
340        let active_session = self.active_session.clone();
341
342        Some(
343            h_flex()
344                .border_b_1()
345                .border_color(cx.theme().colors().border)
346                .p_1()
347                .justify_between()
348                .w_full()
349                .child(
350                    h_flex().gap_2().w_full().when_some(
351                        active_session
352                            .as_ref()
353                            .and_then(|session| session.read(cx).mode().as_running()),
354                        |this, running_session| {
355                            let thread_status = running_session
356                                .read(cx)
357                                .thread_status(cx)
358                                .unwrap_or(project::debugger::session::ThreadStatus::Exited);
359                            let capabilities = running_session.read(cx).capabilities(cx);
360                            this.map(|this| {
361                                if thread_status == ThreadStatus::Running {
362                                    this.child(
363                                        IconButton::new("debug-pause", IconName::DebugPause)
364                                            .icon_size(IconSize::XSmall)
365                                            .shape(ui::IconButtonShape::Square)
366                                            .on_click(window.listener_for(
367                                                &running_session,
368                                                |this, _, _window, cx| {
369                                                    this.pause_thread(cx);
370                                                },
371                                            ))
372                                            .tooltip(move |window, cx| {
373                                                Tooltip::text("Pause program")(window, cx)
374                                            }),
375                                    )
376                                } else {
377                                    this.child(
378                                        IconButton::new("debug-continue", IconName::DebugContinue)
379                                            .icon_size(IconSize::XSmall)
380                                            .shape(ui::IconButtonShape::Square)
381                                            .on_click(window.listener_for(
382                                                &running_session,
383                                                |this, _, _window, cx| this.continue_thread(cx),
384                                            ))
385                                            .disabled(thread_status != ThreadStatus::Stopped)
386                                            .tooltip(move |window, cx| {
387                                                Tooltip::text("Continue program")(window, cx)
388                                            }),
389                                    )
390                                }
391                            })
392                            .child(
393                                IconButton::new("debug-step-over", IconName::ArrowRight)
394                                    .icon_size(IconSize::XSmall)
395                                    .shape(ui::IconButtonShape::Square)
396                                    .on_click(window.listener_for(
397                                        &running_session,
398                                        |this, _, _window, cx| {
399                                            this.step_over(cx);
400                                        },
401                                    ))
402                                    .disabled(thread_status != ThreadStatus::Stopped)
403                                    .tooltip(move |window, cx| {
404                                        Tooltip::text("Step over")(window, cx)
405                                    }),
406                            )
407                            .child(
408                                IconButton::new("debug-step-out", IconName::ArrowUpRight)
409                                    .icon_size(IconSize::XSmall)
410                                    .shape(ui::IconButtonShape::Square)
411                                    .on_click(window.listener_for(
412                                        &running_session,
413                                        |this, _, _window, cx| {
414                                            this.step_out(cx);
415                                        },
416                                    ))
417                                    .disabled(thread_status != ThreadStatus::Stopped)
418                                    .tooltip(move |window, cx| {
419                                        Tooltip::text("Step out")(window, cx)
420                                    }),
421                            )
422                            .child(
423                                IconButton::new("debug-step-into", IconName::ArrowDownRight)
424                                    .icon_size(IconSize::XSmall)
425                                    .shape(ui::IconButtonShape::Square)
426                                    .on_click(window.listener_for(
427                                        &running_session,
428                                        |this, _, _window, cx| {
429                                            this.step_in(cx);
430                                        },
431                                    ))
432                                    .disabled(thread_status != ThreadStatus::Stopped)
433                                    .tooltip(move |window, cx| {
434                                        Tooltip::text("Step in")(window, cx)
435                                    }),
436                            )
437                            .child(Divider::vertical())
438                            .child(
439                                IconButton::new(
440                                    "debug-enable-breakpoint",
441                                    IconName::DebugDisabledBreakpoint,
442                                )
443                                .icon_size(IconSize::XSmall)
444                                .shape(ui::IconButtonShape::Square)
445                                .disabled(thread_status != ThreadStatus::Stopped),
446                            )
447                            .child(
448                                IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
449                                    .icon_size(IconSize::XSmall)
450                                    .shape(ui::IconButtonShape::Square)
451                                    .disabled(thread_status != ThreadStatus::Stopped),
452                            )
453                            .child(
454                                IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
455                                    .icon_size(IconSize::XSmall)
456                                    .shape(ui::IconButtonShape::Square)
457                                    .disabled(
458                                        thread_status == ThreadStatus::Exited
459                                            || thread_status == ThreadStatus::Ended,
460                                    )
461                                    .on_click(window.listener_for(
462                                        &running_session,
463                                        |this, _, _window, cx| {
464                                            this.toggle_ignore_breakpoints(cx);
465                                        },
466                                    ))
467                                    .tooltip(move |window, cx| {
468                                        Tooltip::text("Disable all breakpoints")(window, cx)
469                                    }),
470                            )
471                            .child(Divider::vertical())
472                            .child(
473                                IconButton::new("debug-restart", IconName::DebugRestart)
474                                    .icon_size(IconSize::XSmall)
475                                    .on_click(window.listener_for(
476                                        &running_session,
477                                        |this, _, _window, cx| {
478                                            this.restart_session(cx);
479                                        },
480                                    ))
481                                    .disabled(
482                                        !capabilities.supports_restart_request.unwrap_or_default(),
483                                    )
484                                    .tooltip(move |window, cx| {
485                                        Tooltip::text("Restart")(window, cx)
486                                    }),
487                            )
488                            .child(
489                                IconButton::new("debug-stop", IconName::Power)
490                                    .icon_size(IconSize::XSmall)
491                                    .on_click(window.listener_for(
492                                        &running_session,
493                                        |this, _, _window, cx| {
494                                            this.stop_thread(cx);
495                                        },
496                                    ))
497                                    .disabled(
498                                        thread_status != ThreadStatus::Stopped
499                                            && thread_status != ThreadStatus::Running,
500                                    )
501                                    .tooltip({
502                                        let label = if capabilities
503                                            .supports_terminate_threads_request
504                                            .unwrap_or_default()
505                                        {
506                                            "Terminate Thread"
507                                        } else {
508                                            "Terminate all Threads"
509                                        };
510                                        move |window, cx| Tooltip::text(label)(window, cx)
511                                    }),
512                            )
513                        },
514                    ),
515                )
516                .child(
517                    h_flex()
518                        .gap_2()
519                        .when_some(
520                            active_session
521                                .as_ref()
522                                .and_then(|session| session.read(cx).mode().as_running())
523                                .cloned(),
524                            |this, session| {
525                                this.child(
526                                    session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
527                                )
528                                .child(Divider::vertical())
529                            },
530                        )
531                        .when_some(active_session.as_ref(), |this, session| {
532                            let sessions = self.sessions.clone();
533                            let weak = cx.weak_entity();
534                            let label = session.read(cx).label(cx);
535                            this.child(DropdownMenu::new(
536                                "debugger-session-list",
537                                label,
538                                ContextMenu::build(window, cx, move |mut this, _, cx| {
539                                    for item in sessions {
540                                        let weak = weak.clone();
541                                        this = this.entry(
542                                            session.read(cx).label(cx),
543                                            None,
544                                            move |window, cx| {
545                                                weak.update(cx, |panel, cx| {
546                                                    panel.activate_session(
547                                                        item.clone(),
548                                                        window,
549                                                        cx,
550                                                    );
551                                                })
552                                                .ok();
553                                            },
554                                        );
555                                    }
556                                    this
557                                }),
558                            ))
559                            .child(Divider::vertical())
560                        })
561                        .child(
562                            IconButton::new("debug-new-session", IconName::Plus)
563                                .icon_size(IconSize::Small)
564                                .on_click({
565                                    let workspace = self.workspace.clone();
566                                    let weak_panel = cx.weak_entity();
567                                    let past_debug_definition = self.past_debug_definition.clone();
568                                    move |_, window, cx| {
569                                        let weak_panel = weak_panel.clone();
570                                        let past_debug_definition = past_debug_definition.clone();
571
572                                        let _ = workspace.update(cx, |this, cx| {
573                                            let workspace = cx.weak_entity();
574                                            this.toggle_modal(window, cx, |window, cx| {
575                                                NewSessionModal::new(
576                                                    past_debug_definition,
577                                                    weak_panel,
578                                                    workspace,
579                                                    window,
580                                                    cx,
581                                                )
582                                            });
583                                        });
584                                    }
585                                })
586                                .tooltip(|window, cx| {
587                                    Tooltip::for_action(
588                                        "New Debug Session",
589                                        &CreateDebuggingSession,
590                                        window,
591                                        cx,
592                                    )
593                                }),
594                        ),
595                ),
596        )
597    }
598
599    fn activate_session(
600        &mut self,
601        session_item: Entity<DebugSession>,
602        window: &mut Window,
603        cx: &mut Context<Self>,
604    ) {
605        debug_assert!(self.sessions.contains(&session_item));
606        session_item.focus_handle(cx).focus(window);
607        session_item.update(cx, |this, cx| {
608            if let Some(running) = this.mode().as_running() {
609                running.update(cx, |this, cx| {
610                    this.go_to_selected_stack_frame(window, cx);
611                });
612            }
613        });
614        self.active_session = Some(session_item);
615        cx.notify();
616    }
617}
618
619impl EventEmitter<PanelEvent> for DebugPanel {}
620impl EventEmitter<DebugPanelEvent> for DebugPanel {}
621impl EventEmitter<project::Event> for DebugPanel {}
622
623impl Focusable for DebugPanel {
624    fn focus_handle(&self, _: &App) -> FocusHandle {
625        self.focus_handle.clone()
626    }
627}
628
629impl Panel for DebugPanel {
630    fn persistent_name() -> &'static str {
631        "DebugPanel"
632    }
633
634    fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
635        DockPosition::Bottom
636    }
637
638    fn position_is_valid(&self, position: DockPosition) -> bool {
639        position == DockPosition::Bottom
640    }
641
642    fn set_position(
643        &mut self,
644        _position: DockPosition,
645        _window: &mut Window,
646        _cx: &mut Context<Self>,
647    ) {
648    }
649
650    fn size(&self, _window: &Window, _: &App) -> Pixels {
651        self.size
652    }
653
654    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
655        self.size = size.unwrap();
656    }
657
658    fn remote_id() -> Option<proto::PanelId> {
659        Some(proto::PanelId::DebugPanel)
660    }
661
662    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
663        Some(IconName::Debug)
664    }
665
666    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
667        if DebuggerSettings::get_global(cx).button {
668            Some("Debug Panel")
669        } else {
670            None
671        }
672    }
673
674    fn toggle_action(&self) -> Box<dyn Action> {
675        Box::new(ToggleFocus)
676    }
677
678    fn activation_priority(&self) -> u32 {
679        9
680    }
681    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
682}
683
684impl Render for DebugPanel {
685    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
686        let has_sessions = self.sessions.len() > 0;
687        debug_assert_eq!(has_sessions, self.active_session.is_some());
688
689        v_flex()
690            .size_full()
691            .key_context("DebugPanel")
692            .child(h_flex().children(self.top_controls_strip(window, cx)))
693            .track_focus(&self.focus_handle(cx))
694            .map(|this| {
695                if has_sessions {
696                    this.children(self.active_session.clone())
697                } else {
698                    this.child(
699                        v_flex()
700                            .h_full()
701                            .gap_1()
702                            .items_center()
703                            .justify_center()
704                            .child(
705                                h_flex().child(
706                                    Label::new("No Debugging Sessions")
707                                        .size(LabelSize::Small)
708                                        .color(Color::Muted),
709                                ),
710                            )
711                            .child(
712                                h_flex().flex_shrink().child(
713                                    Button::new("spawn-new-session-empty-state", "New Session")
714                                        .size(ButtonSize::Large)
715                                        .on_click(|_, window, cx| {
716                                            window.dispatch_action(
717                                                CreateDebuggingSession.boxed_clone(),
718                                                cx,
719                                            );
720                                        }),
721                                ),
722                            ),
723                    )
724                }
725            })
726            .into_any()
727    }
728}