debugger_panel.rs

  1use crate::session::DebugSession;
  2use anyhow::{anyhow, Result};
  3use collections::HashMap;
  4use command_palette_hooks::CommandPaletteFilter;
  5use dap::{
  6    client::SessionId, debugger_settings::DebuggerSettings, ContinuedEvent, LoadedSourceEvent,
  7    ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
  8};
  9use futures::{channel::mpsc, SinkExt as _};
 10use gpui::{
 11    actions, Action, App, AsyncWindowContext, Context, Entity, EventEmitter, FocusHandle,
 12    Focusable, Subscription, Task, WeakEntity,
 13};
 14use project::{
 15    debugger::dap_store::{self, DapStore},
 16    terminals::TerminalKind,
 17    Project,
 18};
 19use rpc::proto::{self};
 20use settings::Settings;
 21use std::{any::TypeId, path::PathBuf};
 22use task::DebugTaskDefinition;
 23use terminal_view::terminal_panel::TerminalPanel;
 24use ui::prelude::*;
 25use util::ResultExt;
 26use workspace::{
 27    dock::{DockPosition, Panel, PanelEvent},
 28    pane, ClearAllBreakpoints, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto,
 29    StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, Workspace,
 30};
 31
 32pub enum DebugPanelEvent {
 33    Exited(SessionId),
 34    Terminated(SessionId),
 35    Stopped {
 36        client_id: SessionId,
 37        event: StoppedEvent,
 38        go_to_stack_frame: bool,
 39    },
 40    Thread((SessionId, ThreadEvent)),
 41    Continued((SessionId, ContinuedEvent)),
 42    Output((SessionId, OutputEvent)),
 43    Module((SessionId, ModuleEvent)),
 44    LoadedSource((SessionId, LoadedSourceEvent)),
 45    ClientShutdown(SessionId),
 46    CapabilitiesChanged(SessionId),
 47}
 48
 49actions!(debug_panel, [ToggleFocus]);
 50pub struct DebugPanel {
 51    size: Pixels,
 52    pane: Entity<Pane>,
 53    project: WeakEntity<Project>,
 54    workspace: WeakEntity<Workspace>,
 55    _subscriptions: Vec<Subscription>,
 56    pub(crate) last_inert_config: Option<DebugTaskDefinition>,
 57}
 58
 59impl DebugPanel {
 60    pub fn new(
 61        workspace: &Workspace,
 62        window: &mut Window,
 63        cx: &mut Context<Workspace>,
 64    ) -> Entity<Self> {
 65        cx.new(|cx| {
 66            let project = workspace.project().clone();
 67            let dap_store = project.read(cx).dap_store();
 68            let weak_workspace = workspace.weak_handle();
 69            let debug_panel = cx.weak_entity();
 70            let pane = cx.new(|cx| {
 71                let mut pane = Pane::new(
 72                    workspace.weak_handle(),
 73                    project.clone(),
 74                    Default::default(),
 75                    None,
 76                    gpui::NoAction.boxed_clone(),
 77                    window,
 78                    cx,
 79                );
 80                pane.set_can_split(None);
 81                pane.set_can_navigate(true, cx);
 82                pane.display_nav_history_buttons(None);
 83                pane.set_should_display_tab_bar(|_window, _cx| true);
 84                pane.set_close_pane_if_empty(true, cx);
 85                pane.set_render_tab_bar_buttons(cx, {
 86                    let project = project.clone();
 87                    let weak_workspace = weak_workspace.clone();
 88                    let debug_panel = debug_panel.clone();
 89                    move |_, _, cx| {
 90                        let project = project.clone();
 91                        let weak_workspace = weak_workspace.clone();
 92                        (
 93                            None,
 94                            Some(
 95                                h_flex()
 96                                    .child(
 97                                        IconButton::new("new-debug-session", IconName::Plus)
 98                                            .icon_size(IconSize::Small)
 99                                            .on_click({
100                                                let debug_panel = debug_panel.clone();
101
102                                                cx.listener(move |pane, _, window, cx| {
103                                                    let config = debug_panel
104                                                        .read_with(cx, |this: &DebugPanel, _| {
105                                                            this.last_inert_config.clone()
106                                                        })
107                                                        .log_err()
108                                                        .flatten();
109
110                                                    pane.add_item(
111                                                        Box::new(DebugSession::inert(
112                                                            project.clone(),
113                                                            weak_workspace.clone(),
114                                                            debug_panel.clone(),
115                                                            config,
116                                                            window,
117                                                            cx,
118                                                        )),
119                                                        false,
120                                                        false,
121                                                        None,
122                                                        window,
123                                                        cx,
124                                                    );
125                                                })
126                                            }),
127                                    )
128                                    .into_any_element(),
129                            ),
130                        )
131                    }
132                });
133                pane.add_item(
134                    Box::new(DebugSession::inert(
135                        project.clone(),
136                        weak_workspace.clone(),
137                        debug_panel.clone(),
138                        None,
139                        window,
140                        cx,
141                    )),
142                    false,
143                    false,
144                    None,
145                    window,
146                    cx,
147                );
148                pane
149            });
150
151            let _subscriptions = vec![
152                cx.observe(&pane, |_, _, cx| cx.notify()),
153                cx.subscribe_in(&pane, window, Self::handle_pane_event),
154                cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
155            ];
156
157            let debug_panel = Self {
158                pane,
159                size: px(300.),
160                _subscriptions,
161                last_inert_config: None,
162                project: project.downgrade(),
163                workspace: workspace.weak_handle(),
164            };
165
166            debug_panel
167        })
168    }
169
170    pub fn load(
171        workspace: WeakEntity<Workspace>,
172        cx: AsyncWindowContext,
173    ) -> Task<Result<Entity<Self>>> {
174        cx.spawn(async move |cx| {
175            workspace.update_in(cx, |workspace, window, cx| {
176                let debug_panel = DebugPanel::new(workspace, window, cx);
177
178                workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
179                    workspace.project().read(cx).breakpoint_store().update(
180                        cx,
181                        |breakpoint_store, cx| {
182                            breakpoint_store.clear_breakpoints(cx);
183                        },
184                    )
185                });
186
187                cx.observe(&debug_panel, |_, debug_panel, cx| {
188                    let (has_active_session, supports_restart, support_step_back) = debug_panel
189                        .update(cx, |this, cx| {
190                            this.active_session(cx)
191                                .map(|item| {
192                                    let running = item.read(cx).mode().as_running().cloned();
193
194                                    match running {
195                                        Some(running) => {
196                                            let caps = running.read(cx).capabilities(cx);
197                                            (
198                                                true,
199                                                caps.supports_restart_request.unwrap_or_default(),
200                                                caps.supports_step_back.unwrap_or_default(),
201                                            )
202                                        }
203                                        None => (false, false, false),
204                                    }
205                                })
206                                .unwrap_or((false, false, false))
207                        });
208
209                    let filter = CommandPaletteFilter::global_mut(cx);
210                    let debugger_action_types = [
211                        TypeId::of::<Continue>(),
212                        TypeId::of::<StepOver>(),
213                        TypeId::of::<StepInto>(),
214                        TypeId::of::<StepOut>(),
215                        TypeId::of::<Stop>(),
216                        TypeId::of::<Disconnect>(),
217                        TypeId::of::<Pause>(),
218                        TypeId::of::<ToggleIgnoreBreakpoints>(),
219                    ];
220
221                    let step_back_action_type = [TypeId::of::<StepBack>()];
222                    let restart_action_type = [TypeId::of::<Restart>()];
223
224                    if has_active_session {
225                        filter.show_action_types(debugger_action_types.iter());
226
227                        if supports_restart {
228                            filter.show_action_types(restart_action_type.iter());
229                        } else {
230                            filter.hide_action_types(&restart_action_type);
231                        }
232
233                        if support_step_back {
234                            filter.show_action_types(step_back_action_type.iter());
235                        } else {
236                            filter.hide_action_types(&step_back_action_type);
237                        }
238                    } else {
239                        // show only the `debug: start`
240                        filter.hide_action_types(&debugger_action_types);
241                        filter.hide_action_types(&step_back_action_type);
242                        filter.hide_action_types(&restart_action_type);
243                    }
244                })
245                .detach();
246
247                debug_panel
248            })
249        })
250    }
251
252    pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> {
253        self.pane
254            .read(cx)
255            .active_item()
256            .and_then(|panel| panel.downcast::<DebugSession>())
257    }
258
259    pub fn debug_panel_items_by_client(
260        &self,
261        client_id: &SessionId,
262        cx: &Context<Self>,
263    ) -> Vec<Entity<DebugSession>> {
264        self.pane
265            .read(cx)
266            .items()
267            .filter_map(|item| item.downcast::<DebugSession>())
268            .filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
269            .map(|item| item.clone())
270            .collect()
271    }
272
273    pub fn debug_panel_item_by_client(
274        &self,
275        client_id: SessionId,
276        cx: &mut Context<Self>,
277    ) -> Option<Entity<DebugSession>> {
278        self.pane
279            .read(cx)
280            .items()
281            .filter_map(|item| item.downcast::<DebugSession>())
282            .find(|item| {
283                let item = item.read(cx);
284
285                item.session_id(cx) == Some(client_id)
286            })
287    }
288
289    fn handle_dap_store_event(
290        &mut self,
291        dap_store: &Entity<DapStore>,
292        event: &dap_store::DapStoreEvent,
293        window: &mut Window,
294        cx: &mut Context<Self>,
295    ) {
296        match event {
297            dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
298                let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
299                    return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event");
300                };
301
302                let Some(project) = self.project.upgrade() else {
303                    return log::error!("Debug Panel out lived it's weak reference to Project");
304                };
305
306                if self.pane.read_with(cx, |pane, cx| {
307                    pane.items_of_type::<DebugSession>()
308                        .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
309                }) {
310                    // We already have an item for this session.
311                    return;
312                }
313                let session_item = DebugSession::running(
314                    project,
315                    self.workspace.clone(),
316                    session,
317                    cx.weak_entity(),
318                    window,
319                    cx,
320                );
321
322                self.pane.update(cx, |pane, cx| {
323                    pane.add_item(Box::new(session_item), true, true, None, window, cx);
324                    window.focus(&pane.focus_handle(cx));
325                    cx.notify();
326                });
327            }
328            dap_store::DapStoreEvent::RunInTerminal {
329                title,
330                cwd,
331                command,
332                args,
333                envs,
334                sender,
335                ..
336            } => {
337                self.handle_run_in_terminal_request(
338                    title.clone(),
339                    cwd.clone(),
340                    command.clone(),
341                    args.clone(),
342                    envs.clone(),
343                    sender.clone(),
344                    window,
345                    cx,
346                )
347                .detach_and_log_err(cx);
348            }
349            _ => {}
350        }
351    }
352
353    fn handle_run_in_terminal_request(
354        &self,
355        title: Option<String>,
356        cwd: PathBuf,
357        command: Option<String>,
358        args: Vec<String>,
359        envs: HashMap<String, String>,
360        mut sender: mpsc::Sender<Result<u32>>,
361        window: &mut Window,
362        cx: &mut App,
363    ) -> Task<Result<()>> {
364        let terminal_task = self.workspace.update(cx, |workspace, cx| {
365            let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
366                anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
367            });
368
369            let terminal_panel = match terminal_panel {
370                Ok(panel) => panel,
371                Err(err) => return Task::ready(Err(err)),
372            };
373
374            terminal_panel.update(cx, |terminal_panel, cx| {
375                let terminal_task = terminal_panel.add_terminal(
376                    TerminalKind::Debug {
377                        command,
378                        args,
379                        envs,
380                        cwd,
381                        title,
382                    },
383                    task::RevealStrategy::Always,
384                    window,
385                    cx,
386                );
387
388                cx.spawn(async move |_, cx| {
389                    let pid_task = async move {
390                        let terminal = terminal_task.await?;
391
392                        terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
393                    };
394
395                    pid_task.await
396                })
397            })
398        });
399
400        cx.background_spawn(async move {
401            match terminal_task {
402                Ok(pid_task) => match pid_task.await {
403                    Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
404                    Ok(None) => {
405                        sender
406                            .send(Err(anyhow!(
407                                "Terminal was spawned but PID was not available"
408                            )))
409                            .await?
410                    }
411                    Err(error) => sender.send(Err(anyhow!(error))).await?,
412                },
413                Err(error) => sender.send(Err(anyhow!(error))).await?,
414            };
415
416            Ok(())
417        })
418    }
419
420    fn handle_pane_event(
421        &mut self,
422        _: &Entity<Pane>,
423        event: &pane::Event,
424        window: &mut Window,
425        cx: &mut Context<Self>,
426    ) {
427        match event {
428            pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
429            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
430            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
431            pane::Event::AddItem { item } => {
432                self.workspace
433                    .update(cx, |workspace, cx| {
434                        item.added_to_pane(workspace, self.pane.clone(), window, cx)
435                    })
436                    .ok();
437            }
438            pane::Event::RemovedItem { item } => {
439                if let Some(debug_session) = item.downcast::<DebugSession>() {
440                    debug_session.update(cx, |session, cx| {
441                        session.shutdown(cx);
442                    })
443                }
444            }
445            pane::Event::ActivateItem {
446                local: _,
447                focus_changed,
448            } => {
449                if *focus_changed {
450                    if let Some(debug_session) = self
451                        .pane
452                        .read(cx)
453                        .active_item()
454                        .and_then(|item| item.downcast::<DebugSession>())
455                    {
456                        if let Some(running) = debug_session
457                            .read_with(cx, |session, _| session.mode().as_running().cloned())
458                        {
459                            running.update(cx, |running, cx| {
460                                running.go_to_selected_stack_frame(window, cx);
461                            });
462                        }
463                    }
464                }
465            }
466
467            _ => {}
468        }
469    }
470}
471
472impl EventEmitter<PanelEvent> for DebugPanel {}
473impl EventEmitter<DebugPanelEvent> for DebugPanel {}
474impl EventEmitter<project::Event> for DebugPanel {}
475
476impl Focusable for DebugPanel {
477    fn focus_handle(&self, cx: &App) -> FocusHandle {
478        self.pane.focus_handle(cx)
479    }
480}
481
482impl Panel for DebugPanel {
483    fn pane(&self) -> Option<Entity<Pane>> {
484        Some(self.pane.clone())
485    }
486
487    fn persistent_name() -> &'static str {
488        "DebugPanel"
489    }
490
491    fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
492        DockPosition::Bottom
493    }
494
495    fn position_is_valid(&self, position: DockPosition) -> bool {
496        position == DockPosition::Bottom
497    }
498
499    fn set_position(
500        &mut self,
501        _position: DockPosition,
502        _window: &mut Window,
503        _cx: &mut Context<Self>,
504    ) {
505    }
506
507    fn size(&self, _window: &Window, _cx: &App) -> Pixels {
508        self.size
509    }
510
511    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
512        self.size = size.unwrap();
513    }
514
515    fn remote_id() -> Option<proto::PanelId> {
516        Some(proto::PanelId::DebugPanel)
517    }
518
519    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
520        Some(IconName::Debug)
521    }
522
523    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
524        if DebuggerSettings::get_global(cx).button {
525            Some("Debug Panel")
526        } else {
527            None
528        }
529    }
530
531    fn toggle_action(&self) -> Box<dyn Action> {
532        Box::new(ToggleFocus)
533    }
534
535    fn activation_priority(&self) -> u32 {
536        9
537    }
538    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
539        if active && self.pane.read(cx).items_len() == 0 {
540            let Some(project) = self.project.clone().upgrade() else {
541                return;
542            };
543            let config = self.last_inert_config.clone();
544            let panel = cx.weak_entity();
545            // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
546            self.pane.update(cx, |this, cx| {
547                this.add_item(
548                    Box::new(DebugSession::inert(
549                        project,
550                        self.workspace.clone(),
551                        panel,
552                        config,
553                        window,
554                        cx,
555                    )),
556                    false,
557                    false,
558                    None,
559                    window,
560                    cx,
561                );
562            });
563        }
564    }
565}
566
567impl Render for DebugPanel {
568    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
569        v_flex()
570            .key_context("DebugPanel")
571            .track_focus(&self.focus_handle(cx))
572            .size_full()
573            .child(self.pane.clone())
574            .into_any()
575    }
576}