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