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