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}