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!(
300                        "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
301                    );
302                };
303
304                let Some(project) = self.project.upgrade() else {
305                    return log::error!("Debug Panel out lived it's weak reference to Project");
306                };
307
308                if self.pane.read_with(cx, |pane, cx| {
309                    pane.items_of_type::<DebugSession>()
310                        .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
311                }) {
312                    // We already have an item for this session.
313                    return;
314                }
315                let session_item = DebugSession::running(
316                    project,
317                    self.workspace.clone(),
318                    session,
319                    cx.weak_entity(),
320                    window,
321                    cx,
322                );
323
324                self.pane.update(cx, |pane, cx| {
325                    pane.add_item(Box::new(session_item), true, true, None, window, cx);
326                    window.focus(&pane.focus_handle(cx));
327                    cx.notify();
328                });
329            }
330            dap_store::DapStoreEvent::RunInTerminal {
331                title,
332                cwd,
333                command,
334                args,
335                envs,
336                sender,
337                ..
338            } => {
339                self.handle_run_in_terminal_request(
340                    title.clone(),
341                    cwd.clone(),
342                    command.clone(),
343                    args.clone(),
344                    envs.clone(),
345                    sender.clone(),
346                    window,
347                    cx,
348                )
349                .detach_and_log_err(cx);
350            }
351            _ => {}
352        }
353    }
354
355    fn handle_run_in_terminal_request(
356        &self,
357        title: Option<String>,
358        cwd: PathBuf,
359        command: Option<String>,
360        args: Vec<String>,
361        envs: HashMap<String, String>,
362        mut sender: mpsc::Sender<Result<u32>>,
363        window: &mut Window,
364        cx: &mut App,
365    ) -> Task<Result<()>> {
366        let terminal_task = self.workspace.update(cx, |workspace, cx| {
367            let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
368                anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
369            });
370
371            let terminal_panel = match terminal_panel {
372                Ok(panel) => panel,
373                Err(err) => return Task::ready(Err(err)),
374            };
375
376            terminal_panel.update(cx, |terminal_panel, cx| {
377                let terminal_task = terminal_panel.add_terminal(
378                    TerminalKind::Debug {
379                        command,
380                        args,
381                        envs,
382                        cwd,
383                        title,
384                    },
385                    task::RevealStrategy::Always,
386                    window,
387                    cx,
388                );
389
390                cx.spawn(async move |_, cx| {
391                    let pid_task = async move {
392                        let terminal = terminal_task.await?;
393
394                        terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
395                    };
396
397                    pid_task.await
398                })
399            })
400        });
401
402        cx.background_spawn(async move {
403            match terminal_task {
404                Ok(pid_task) => match pid_task.await {
405                    Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
406                    Ok(None) => {
407                        sender
408                            .send(Err(anyhow!(
409                                "Terminal was spawned but PID was not available"
410                            )))
411                            .await?
412                    }
413                    Err(error) => sender.send(Err(anyhow!(error))).await?,
414                },
415                Err(error) => sender.send(Err(anyhow!(error))).await?,
416            };
417
418            Ok(())
419        })
420    }
421
422    fn handle_pane_event(
423        &mut self,
424        _: &Entity<Pane>,
425        event: &pane::Event,
426        window: &mut Window,
427        cx: &mut Context<Self>,
428    ) {
429        match event {
430            pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
431            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
432            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
433            pane::Event::AddItem { item } => {
434                self.workspace
435                    .update(cx, |workspace, cx| {
436                        item.added_to_pane(workspace, self.pane.clone(), window, cx)
437                    })
438                    .ok();
439            }
440            pane::Event::RemovedItem { item } => {
441                if let Some(debug_session) = item.downcast::<DebugSession>() {
442                    debug_session.update(cx, |session, cx| {
443                        session.shutdown(cx);
444                    })
445                }
446            }
447            pane::Event::ActivateItem {
448                local: _,
449                focus_changed,
450            } => {
451                if *focus_changed {
452                    if let Some(debug_session) = self
453                        .pane
454                        .read(cx)
455                        .active_item()
456                        .and_then(|item| item.downcast::<DebugSession>())
457                    {
458                        if let Some(running) = debug_session
459                            .read_with(cx, |session, _| session.mode().as_running().cloned())
460                        {
461                            running.update(cx, |running, cx| {
462                                running.go_to_selected_stack_frame(window, cx);
463                            });
464                        }
465                    }
466                }
467            }
468
469            _ => {}
470        }
471    }
472}
473
474impl EventEmitter<PanelEvent> for DebugPanel {}
475impl EventEmitter<DebugPanelEvent> for DebugPanel {}
476impl EventEmitter<project::Event> for DebugPanel {}
477
478impl Focusable for DebugPanel {
479    fn focus_handle(&self, cx: &App) -> FocusHandle {
480        self.pane.focus_handle(cx)
481    }
482}
483
484impl Panel for DebugPanel {
485    fn pane(&self) -> Option<Entity<Pane>> {
486        Some(self.pane.clone())
487    }
488
489    fn persistent_name() -> &'static str {
490        "DebugPanel"
491    }
492
493    fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
494        DockPosition::Bottom
495    }
496
497    fn position_is_valid(&self, position: DockPosition) -> bool {
498        position == DockPosition::Bottom
499    }
500
501    fn set_position(
502        &mut self,
503        _position: DockPosition,
504        _window: &mut Window,
505        _cx: &mut Context<Self>,
506    ) {
507    }
508
509    fn size(&self, _window: &Window, _cx: &App) -> Pixels {
510        self.size
511    }
512
513    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
514        self.size = size.unwrap();
515    }
516
517    fn remote_id() -> Option<proto::PanelId> {
518        Some(proto::PanelId::DebugPanel)
519    }
520
521    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
522        Some(IconName::Debug)
523    }
524
525    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
526        if DebuggerSettings::get_global(cx).button {
527            Some("Debug Panel")
528        } else {
529            None
530        }
531    }
532
533    fn toggle_action(&self) -> Box<dyn Action> {
534        Box::new(ToggleFocus)
535    }
536
537    fn activation_priority(&self) -> u32 {
538        9
539    }
540    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
541        if active && self.pane.read(cx).items_len() == 0 {
542            let Some(project) = self.project.clone().upgrade() else {
543                return;
544            };
545            let config = self.last_inert_config.clone();
546            let panel = cx.weak_entity();
547            // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
548            self.pane.update(cx, |this, cx| {
549                this.add_item(
550                    Box::new(DebugSession::inert(
551                        project,
552                        self.workspace.clone(),
553                        panel,
554                        config,
555                        window,
556                        cx,
557                    )),
558                    false,
559                    false,
560                    None,
561                    window,
562                    cx,
563                );
564            });
565        }
566    }
567}
568
569impl Render for DebugPanel {
570    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
571        v_flex()
572            .key_context("DebugPanel")
573            .track_focus(&self.focus_handle(cx))
574            .size_full()
575            .child(self.pane.clone())
576            .into_any()
577    }
578}