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}