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}