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!("Couldn't get session with id: {session_id:?} from DebugClientStarted event");
300 };
301
302 let Some(project) = self.project.upgrade() else {
303 return log::error!("Debug Panel out lived it's weak reference to Project");
304 };
305
306 if self.pane.read_with(cx, |pane, cx| {
307 pane.items_of_type::<DebugSession>()
308 .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
309 }) {
310 // We already have an item for this session.
311 return;
312 }
313 let session_item = DebugSession::running(
314 project,
315 self.workspace.clone(),
316 session,
317 cx.weak_entity(),
318 window,
319 cx,
320 );
321
322 self.pane.update(cx, |pane, cx| {
323 pane.add_item(Box::new(session_item), true, true, None, window, cx);
324 window.focus(&pane.focus_handle(cx));
325 cx.notify();
326 });
327 }
328 dap_store::DapStoreEvent::RunInTerminal {
329 title,
330 cwd,
331 command,
332 args,
333 envs,
334 sender,
335 ..
336 } => {
337 self.handle_run_in_terminal_request(
338 title.clone(),
339 cwd.clone(),
340 command.clone(),
341 args.clone(),
342 envs.clone(),
343 sender.clone(),
344 window,
345 cx,
346 )
347 .detach_and_log_err(cx);
348 }
349 _ => {}
350 }
351 }
352
353 fn handle_run_in_terminal_request(
354 &self,
355 title: Option<String>,
356 cwd: PathBuf,
357 command: Option<String>,
358 args: Vec<String>,
359 envs: HashMap<String, String>,
360 mut sender: mpsc::Sender<Result<u32>>,
361 window: &mut Window,
362 cx: &mut App,
363 ) -> Task<Result<()>> {
364 let terminal_task = self.workspace.update(cx, |workspace, cx| {
365 let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
366 anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
367 });
368
369 let terminal_panel = match terminal_panel {
370 Ok(panel) => panel,
371 Err(err) => return Task::ready(Err(err)),
372 };
373
374 terminal_panel.update(cx, |terminal_panel, cx| {
375 let terminal_task = terminal_panel.add_terminal(
376 TerminalKind::Debug {
377 command,
378 args,
379 envs,
380 cwd,
381 title,
382 },
383 task::RevealStrategy::Always,
384 window,
385 cx,
386 );
387
388 cx.spawn(async move |_, cx| {
389 let pid_task = async move {
390 let terminal = terminal_task.await?;
391
392 terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
393 };
394
395 pid_task.await
396 })
397 })
398 });
399
400 cx.background_spawn(async move {
401 match terminal_task {
402 Ok(pid_task) => match pid_task.await {
403 Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
404 Ok(None) => {
405 sender
406 .send(Err(anyhow!(
407 "Terminal was spawned but PID was not available"
408 )))
409 .await?
410 }
411 Err(error) => sender.send(Err(anyhow!(error))).await?,
412 },
413 Err(error) => sender.send(Err(anyhow!(error))).await?,
414 };
415
416 Ok(())
417 })
418 }
419
420 fn handle_pane_event(
421 &mut self,
422 _: &Entity<Pane>,
423 event: &pane::Event,
424 window: &mut Window,
425 cx: &mut Context<Self>,
426 ) {
427 match event {
428 pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
429 pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
430 pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
431 pane::Event::AddItem { item } => {
432 self.workspace
433 .update(cx, |workspace, cx| {
434 item.added_to_pane(workspace, self.pane.clone(), window, cx)
435 })
436 .ok();
437 }
438 pane::Event::RemovedItem { item } => {
439 if let Some(debug_session) = item.downcast::<DebugSession>() {
440 debug_session.update(cx, |session, cx| {
441 session.shutdown(cx);
442 })
443 }
444 }
445 pane::Event::ActivateItem {
446 local: _,
447 focus_changed,
448 } => {
449 if *focus_changed {
450 if let Some(debug_session) = self
451 .pane
452 .read(cx)
453 .active_item()
454 .and_then(|item| item.downcast::<DebugSession>())
455 {
456 if let Some(running) = debug_session
457 .read_with(cx, |session, _| session.mode().as_running().cloned())
458 {
459 running.update(cx, |running, cx| {
460 running.go_to_selected_stack_frame(window, cx);
461 });
462 }
463 }
464 }
465 }
466
467 _ => {}
468 }
469 }
470}
471
472impl EventEmitter<PanelEvent> for DebugPanel {}
473impl EventEmitter<DebugPanelEvent> for DebugPanel {}
474impl EventEmitter<project::Event> for DebugPanel {}
475
476impl Focusable for DebugPanel {
477 fn focus_handle(&self, cx: &App) -> FocusHandle {
478 self.pane.focus_handle(cx)
479 }
480}
481
482impl Panel for DebugPanel {
483 fn pane(&self) -> Option<Entity<Pane>> {
484 Some(self.pane.clone())
485 }
486
487 fn persistent_name() -> &'static str {
488 "DebugPanel"
489 }
490
491 fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
492 DockPosition::Bottom
493 }
494
495 fn position_is_valid(&self, position: DockPosition) -> bool {
496 position == DockPosition::Bottom
497 }
498
499 fn set_position(
500 &mut self,
501 _position: DockPosition,
502 _window: &mut Window,
503 _cx: &mut Context<Self>,
504 ) {
505 }
506
507 fn size(&self, _window: &Window, _cx: &App) -> Pixels {
508 self.size
509 }
510
511 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
512 self.size = size.unwrap();
513 }
514
515 fn remote_id() -> Option<proto::PanelId> {
516 Some(proto::PanelId::DebugPanel)
517 }
518
519 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
520 Some(IconName::Debug)
521 }
522
523 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
524 if DebuggerSettings::get_global(cx).button {
525 Some("Debug Panel")
526 } else {
527 None
528 }
529 }
530
531 fn toggle_action(&self) -> Box<dyn Action> {
532 Box::new(ToggleFocus)
533 }
534
535 fn activation_priority(&self) -> u32 {
536 9
537 }
538 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
539 if active && self.pane.read(cx).items_len() == 0 {
540 let Some(project) = self.project.clone().upgrade() else {
541 return;
542 };
543 let config = self.last_inert_config.clone();
544 let panel = cx.weak_entity();
545 // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
546 self.pane.update(cx, |this, cx| {
547 this.add_item(
548 Box::new(DebugSession::inert(
549 project,
550 self.workspace.clone(),
551 panel,
552 config,
553 window,
554 cx,
555 )),
556 false,
557 false,
558 None,
559 window,
560 cx,
561 );
562 });
563 }
564 }
565}
566
567impl Render for DebugPanel {
568 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
569 v_flex()
570 .key_context("DebugPanel")
571 .track_focus(&self.focus_handle(cx))
572 .size_full()
573 .child(self.pane.clone())
574 .into_any()
575 }
576}