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