debugger_panel.rs

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