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, ClearAllBreakpoints, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto,
 28    StepOut, StepOver, Stop, 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                workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
178                    workspace.project().read(cx).breakpoint_store().update(
179                        cx,
180                        |breakpoint_store, cx| {
181                            breakpoint_store.clear_breakpoints(cx);
182                        },
183                    )
184                });
185
186                cx.observe(&debug_panel, |_, debug_panel, cx| {
187                    let (has_active_session, supports_restart, support_step_back) = debug_panel
188                        .update(cx, |this, cx| {
189                            this.active_session(cx)
190                                .map(|item| {
191                                    let running = item.read(cx).mode().as_running().cloned();
192
193                                    match running {
194                                        Some(running) => {
195                                            let caps = running.read(cx).capabilities(cx);
196                                            (
197                                                true,
198                                                caps.supports_restart_request.unwrap_or_default(),
199                                                caps.supports_step_back.unwrap_or_default(),
200                                            )
201                                        }
202                                        None => (false, false, false),
203                                    }
204                                })
205                                .unwrap_or((false, false, false))
206                        });
207
208                    let filter = CommandPaletteFilter::global_mut(cx);
209                    let debugger_action_types = [
210                        TypeId::of::<Continue>(),
211                        TypeId::of::<StepOver>(),
212                        TypeId::of::<StepInto>(),
213                        TypeId::of::<StepOut>(),
214                        TypeId::of::<Stop>(),
215                        TypeId::of::<Disconnect>(),
216                        TypeId::of::<Pause>(),
217                        TypeId::of::<ToggleIgnoreBreakpoints>(),
218                    ];
219
220                    let step_back_action_type = [TypeId::of::<StepBack>()];
221                    let restart_action_type = [TypeId::of::<Restart>()];
222
223                    if has_active_session {
224                        filter.show_action_types(debugger_action_types.iter());
225
226                        if supports_restart {
227                            filter.show_action_types(restart_action_type.iter());
228                        } else {
229                            filter.hide_action_types(&restart_action_type);
230                        }
231
232                        if support_step_back {
233                            filter.show_action_types(step_back_action_type.iter());
234                        } else {
235                            filter.hide_action_types(&step_back_action_type);
236                        }
237                    } else {
238                        // show only the `debug: start`
239                        filter.hide_action_types(&debugger_action_types);
240                        filter.hide_action_types(&step_back_action_type);
241                        filter.hide_action_types(&restart_action_type);
242                    }
243                })
244                .detach();
245
246                debug_panel
247            })
248        })
249    }
250
251    pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> {
252        self.pane
253            .read(cx)
254            .active_item()
255            .and_then(|panel| panel.downcast::<DebugSession>())
256    }
257
258    pub fn debug_panel_items_by_client(
259        &self,
260        client_id: &SessionId,
261        cx: &Context<Self>,
262    ) -> Vec<Entity<DebugSession>> {
263        self.pane
264            .read(cx)
265            .items()
266            .filter_map(|item| item.downcast::<DebugSession>())
267            .filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
268            .map(|item| item.clone())
269            .collect()
270    }
271
272    pub fn debug_panel_item_by_client(
273        &self,
274        client_id: SessionId,
275        cx: &mut Context<Self>,
276    ) -> Option<Entity<DebugSession>> {
277        self.pane
278            .read(cx)
279            .items()
280            .filter_map(|item| item.downcast::<DebugSession>())
281            .find(|item| {
282                let item = item.read(cx);
283
284                item.session_id(cx) == Some(client_id)
285            })
286    }
287
288    fn handle_dap_store_event(
289        &mut self,
290        dap_store: &Entity<DapStore>,
291        event: &dap_store::DapStoreEvent,
292        window: &mut Window,
293        cx: &mut Context<Self>,
294    ) {
295        match event {
296            dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
297                let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
298                    return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event");
299                };
300
301                let Some(project) = self.project.upgrade() else {
302                    return log::error!("Debug Panel out lived it's weak reference to Project");
303                };
304
305                if self.pane.read_with(cx, |pane, cx| {
306                    pane.items_of_type::<DebugSession>()
307                        .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
308                }) {
309                    // We already have an item for this session.
310                    return;
311                }
312                let session_item = DebugSession::running(
313                    project,
314                    self.workspace.clone(),
315                    session,
316                    cx.weak_entity(),
317                    window,
318                    cx,
319                );
320
321                self.pane.update(cx, |pane, cx| {
322                    pane.add_item(Box::new(session_item), true, true, None, window, cx);
323                    window.focus(&pane.focus_handle(cx));
324                    cx.notify();
325                });
326            }
327            dap_store::DapStoreEvent::RunInTerminal {
328                title,
329                cwd,
330                command,
331                args,
332                envs,
333                sender,
334                ..
335            } => {
336                self.handle_run_in_terminal_request(
337                    title.clone(),
338                    cwd.clone(),
339                    command.clone(),
340                    args.clone(),
341                    envs.clone(),
342                    sender.clone(),
343                    window,
344                    cx,
345                )
346                .detach_and_log_err(cx);
347            }
348            _ => {}
349        }
350    }
351
352    fn handle_run_in_terminal_request(
353        &self,
354        title: Option<String>,
355        cwd: PathBuf,
356        command: Option<String>,
357        args: Vec<String>,
358        envs: HashMap<String, String>,
359        mut sender: mpsc::Sender<Result<u32>>,
360        window: &mut Window,
361        cx: &mut App,
362    ) -> Task<Result<()>> {
363        let terminal_task = self.workspace.update(cx, |workspace, cx| {
364            let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
365                anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
366            });
367
368            let terminal_panel = match terminal_panel {
369                Ok(panel) => panel,
370                Err(err) => return Task::ready(Err(err)),
371            };
372
373            terminal_panel.update(cx, |terminal_panel, cx| {
374                let terminal_task = terminal_panel.add_terminal(
375                    TerminalKind::Debug {
376                        command,
377                        args,
378                        envs,
379                        cwd,
380                        title,
381                    },
382                    task::RevealStrategy::Always,
383                    window,
384                    cx,
385                );
386
387                cx.spawn(async move |_, cx| {
388                    let pid_task = async move {
389                        let terminal = terminal_task.await?;
390
391                        terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
392                    };
393
394                    pid_task.await
395                })
396            })
397        });
398
399        cx.background_spawn(async move {
400            match terminal_task {
401                Ok(pid_task) => match pid_task.await {
402                    Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
403                    Ok(None) => {
404                        sender
405                            .send(Err(anyhow!(
406                                "Terminal was spawned but PID was not available"
407                            )))
408                            .await?
409                    }
410                    Err(error) => sender.send(Err(anyhow!(error))).await?,
411                },
412                Err(error) => sender.send(Err(anyhow!(error))).await?,
413            };
414
415            Ok(())
416        })
417    }
418
419    fn handle_pane_event(
420        &mut self,
421        _: &Entity<Pane>,
422        event: &pane::Event,
423        window: &mut Window,
424        cx: &mut Context<Self>,
425    ) {
426        match event {
427            pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
428            pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
429            pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
430            pane::Event::AddItem { item } => {
431                self.workspace
432                    .update(cx, |workspace, cx| {
433                        item.added_to_pane(workspace, self.pane.clone(), window, cx)
434                    })
435                    .ok();
436            }
437            pane::Event::RemovedItem { item } => {
438                if let Some(debug_session) = item.downcast::<DebugSession>() {
439                    debug_session.update(cx, |session, cx| {
440                        session.shutdown(cx);
441                    })
442                }
443            }
444            pane::Event::ActivateItem {
445                local: _,
446                focus_changed,
447            } => {
448                if *focus_changed {
449                    if let Some(debug_session) = self
450                        .pane
451                        .read(cx)
452                        .active_item()
453                        .and_then(|item| item.downcast::<DebugSession>())
454                    {
455                        if let Some(running) = debug_session
456                            .read_with(cx, |session, _| session.mode().as_running().cloned())
457                        {
458                            running.update(cx, |running, cx| {
459                                running.go_to_selected_stack_frame(window, cx);
460                            });
461                        }
462                    }
463                }
464            }
465
466            _ => {}
467        }
468    }
469}
470
471impl EventEmitter<PanelEvent> for DebugPanel {}
472impl EventEmitter<DebugPanelEvent> for DebugPanel {}
473impl EventEmitter<project::Event> for DebugPanel {}
474
475impl Focusable for DebugPanel {
476    fn focus_handle(&self, cx: &App) -> FocusHandle {
477        self.pane.focus_handle(cx)
478    }
479}
480
481impl Panel for DebugPanel {
482    fn pane(&self) -> Option<Entity<Pane>> {
483        Some(self.pane.clone())
484    }
485
486    fn persistent_name() -> &'static str {
487        "DebugPanel"
488    }
489
490    fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
491        DockPosition::Bottom
492    }
493
494    fn position_is_valid(&self, position: DockPosition) -> bool {
495        position == DockPosition::Bottom
496    }
497
498    fn set_position(
499        &mut self,
500        _position: DockPosition,
501        _window: &mut Window,
502        _cx: &mut Context<Self>,
503    ) {
504    }
505
506    fn size(&self, _window: &Window, _cx: &App) -> Pixels {
507        self.size
508    }
509
510    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
511        self.size = size.unwrap();
512    }
513
514    fn remote_id() -> Option<proto::PanelId> {
515        Some(proto::PanelId::DebugPanel)
516    }
517
518    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
519        Some(IconName::Debug)
520    }
521
522    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
523        if DebuggerSettings::get_global(cx).button {
524            Some("Debug Panel")
525        } else {
526            None
527        }
528    }
529
530    fn toggle_action(&self) -> Box<dyn Action> {
531        Box::new(ToggleFocus)
532    }
533
534    fn activation_priority(&self) -> u32 {
535        9
536    }
537    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
538        if active && self.pane.read(cx).items_len() == 0 {
539            let Some(project) = self.project.clone().upgrade() else {
540                return;
541            };
542            let config = self.last_inert_config.clone();
543            let panel = cx.weak_entity();
544            // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
545            self.pane.update(cx, |this, cx| {
546                this.add_item(
547                    Box::new(DebugSession::inert(
548                        project,
549                        self.workspace.clone(),
550                        panel,
551                        config,
552                        window,
553                        cx,
554                    )),
555                    false,
556                    false,
557                    None,
558                    window,
559                    cx,
560                );
561            });
562        }
563    }
564}
565
566impl Render for DebugPanel {
567    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
568        v_flex()
569            .key_context("DebugPanel")
570            .track_focus(&self.focus_handle(cx))
571            .size_full()
572            .child(self.pane.clone())
573            .into_any()
574    }
575}