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 terminal_view::terminal_panel::TerminalPanel;
23use ui::prelude::*;
24use workspace::{
25 dock::{DockPosition, Panel, PanelEvent},
26 pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop,
27 ToggleIgnoreBreakpoints, Workspace,
28};
29
30pub enum DebugPanelEvent {
31 Exited(SessionId),
32 Terminated(SessionId),
33 Stopped {
34 client_id: SessionId,
35 event: StoppedEvent,
36 go_to_stack_frame: bool,
37 },
38 Thread((SessionId, ThreadEvent)),
39 Continued((SessionId, ContinuedEvent)),
40 Output((SessionId, OutputEvent)),
41 Module((SessionId, ModuleEvent)),
42 LoadedSource((SessionId, LoadedSourceEvent)),
43 ClientShutdown(SessionId),
44 CapabilitiesChanged(SessionId),
45}
46
47actions!(debug_panel, [ToggleFocus]);
48pub struct DebugPanel {
49 size: Pixels,
50 pane: Entity<Pane>,
51 project: WeakEntity<Project>,
52 workspace: WeakEntity<Workspace>,
53 _subscriptions: Vec<Subscription>,
54}
55
56impl DebugPanel {
57 pub fn new(
58 workspace: &Workspace,
59 window: &mut Window,
60 cx: &mut Context<Workspace>,
61 ) -> Entity<Self> {
62 cx.new(|cx| {
63 let project = workspace.project().clone();
64 let dap_store = project.read(cx).dap_store();
65 let weak_workspace = workspace.weak_handle();
66 let pane = cx.new(|cx| {
67 let mut pane = Pane::new(
68 workspace.weak_handle(),
69 project.clone(),
70 Default::default(),
71 None,
72 gpui::NoAction.boxed_clone(),
73 window,
74 cx,
75 );
76 pane.set_can_split(None);
77 pane.set_can_navigate(true, cx);
78 pane.display_nav_history_buttons(None);
79 pane.set_should_display_tab_bar(|_window, _cx| true);
80 pane.set_close_pane_if_empty(true, cx);
81 pane.set_render_tab_bar_buttons(cx, {
82 let project = project.clone();
83 let weak_workspace = weak_workspace.clone();
84 move |_, _, cx| {
85 let project = project.clone();
86 let weak_workspace = weak_workspace.clone();
87 (
88 None,
89 Some(
90 h_flex()
91 .child(
92 IconButton::new("new-debug-session", IconName::Plus)
93 .icon_size(IconSize::Small)
94 .on_click(cx.listener(move |pane, _, window, cx| {
95 pane.add_item(
96 Box::new(DebugSession::inert(
97 project.clone(),
98 weak_workspace.clone(),
99 window,
100 cx,
101 )),
102 false,
103 false,
104 None,
105 window,
106 cx,
107 );
108 })),
109 )
110 .into_any_element(),
111 ),
112 )
113 }
114 });
115 pane.add_item(
116 Box::new(DebugSession::inert(
117 project.clone(),
118 weak_workspace.clone(),
119 window,
120 cx,
121 )),
122 false,
123 false,
124 None,
125 window,
126 cx,
127 );
128 pane
129 });
130
131 let _subscriptions = vec![
132 cx.observe(&pane, |_, _, cx| cx.notify()),
133 cx.subscribe_in(&pane, window, Self::handle_pane_event),
134 cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
135 ];
136
137 let debug_panel = Self {
138 pane,
139 size: px(300.),
140 _subscriptions,
141 project: project.downgrade(),
142 workspace: workspace.weak_handle(),
143 };
144
145 debug_panel
146 })
147 }
148
149 pub fn load(
150 workspace: WeakEntity<Workspace>,
151 cx: AsyncWindowContext,
152 ) -> Task<Result<Entity<Self>>> {
153 cx.spawn(|mut cx| async move {
154 workspace.update_in(&mut cx, |workspace, window, cx| {
155 let debug_panel = DebugPanel::new(workspace, window, cx);
156
157 cx.observe(&debug_panel, |_, debug_panel, cx| {
158 let (has_active_session, supports_restart, support_step_back) = debug_panel
159 .update(cx, |this, cx| {
160 this.active_session(cx)
161 .map(|item| {
162 let running = item.read(cx).mode().as_running().cloned();
163
164 match running {
165 Some(running) => {
166 let caps = running.read(cx).capabilities(cx);
167 (
168 true,
169 caps.supports_restart_request.unwrap_or_default(),
170 caps.supports_step_back.unwrap_or_default(),
171 )
172 }
173 None => (false, false, false),
174 }
175 })
176 .unwrap_or((false, false, false))
177 });
178
179 let filter = CommandPaletteFilter::global_mut(cx);
180 let debugger_action_types = [
181 TypeId::of::<Continue>(),
182 TypeId::of::<StepOver>(),
183 TypeId::of::<StepInto>(),
184 TypeId::of::<StepOut>(),
185 TypeId::of::<Stop>(),
186 TypeId::of::<Disconnect>(),
187 TypeId::of::<Pause>(),
188 TypeId::of::<ToggleIgnoreBreakpoints>(),
189 ];
190
191 let step_back_action_type = [TypeId::of::<StepBack>()];
192 let restart_action_type = [TypeId::of::<Restart>()];
193
194 if has_active_session {
195 filter.show_action_types(debugger_action_types.iter());
196
197 if supports_restart {
198 filter.show_action_types(restart_action_type.iter());
199 } else {
200 filter.hide_action_types(&restart_action_type);
201 }
202
203 if support_step_back {
204 filter.show_action_types(step_back_action_type.iter());
205 } else {
206 filter.hide_action_types(&step_back_action_type);
207 }
208 } else {
209 // show only the `debug: start`
210 filter.hide_action_types(&debugger_action_types);
211 filter.hide_action_types(&step_back_action_type);
212 filter.hide_action_types(&restart_action_type);
213 }
214 })
215 .detach();
216
217 debug_panel
218 })
219 })
220 }
221
222 pub fn active_session(&self, cx: &App) -> Option<Entity<DebugSession>> {
223 self.pane
224 .read(cx)
225 .active_item()
226 .and_then(|panel| panel.downcast::<DebugSession>())
227 }
228
229 pub fn debug_panel_items_by_client(
230 &self,
231 client_id: &SessionId,
232 cx: &Context<Self>,
233 ) -> Vec<Entity<DebugSession>> {
234 self.pane
235 .read(cx)
236 .items()
237 .filter_map(|item| item.downcast::<DebugSession>())
238 .filter(|item| item.read(cx).session_id(cx) == Some(*client_id))
239 .map(|item| item.clone())
240 .collect()
241 }
242
243 pub fn debug_panel_item_by_client(
244 &self,
245 client_id: SessionId,
246 cx: &mut Context<Self>,
247 ) -> Option<Entity<DebugSession>> {
248 self.pane
249 .read(cx)
250 .items()
251 .filter_map(|item| item.downcast::<DebugSession>())
252 .find(|item| {
253 let item = item.read(cx);
254
255 item.session_id(cx) == Some(client_id)
256 })
257 }
258
259 fn handle_dap_store_event(
260 &mut self,
261 dap_store: &Entity<DapStore>,
262 event: &dap_store::DapStoreEvent,
263 window: &mut Window,
264 cx: &mut Context<Self>,
265 ) {
266 match event {
267 dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
268 let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
269 return log::error!("Couldn't get session with id: {session_id:?} from DebugClientStarted event");
270 };
271
272 let Some(project) = self.project.upgrade() else {
273 return log::error!("Debug Panel out lived it's weak reference to Project");
274 };
275
276 if self.pane.read_with(cx, |pane, cx| {
277 pane.items_of_type::<DebugSession>()
278 .any(|item| item.read(cx).session_id(cx) == Some(*session_id))
279 }) {
280 // We already have an item for this session.
281 return;
282 }
283 let session_item =
284 DebugSession::running(project, self.workspace.clone(), session, window, cx);
285
286 self.pane.update(cx, |pane, cx| {
287 pane.add_item(Box::new(session_item), true, true, None, window, cx);
288 window.focus(&pane.focus_handle(cx));
289 cx.notify();
290 });
291 }
292 dap_store::DapStoreEvent::RunInTerminal {
293 title,
294 cwd,
295 command,
296 args,
297 envs,
298 sender,
299 ..
300 } => {
301 self.handle_run_in_terminal_request(
302 title.clone(),
303 cwd.clone(),
304 command.clone(),
305 args.clone(),
306 envs.clone(),
307 sender.clone(),
308 window,
309 cx,
310 )
311 .detach_and_log_err(cx);
312 }
313 _ => {}
314 }
315 }
316
317 fn handle_run_in_terminal_request(
318 &self,
319 title: Option<String>,
320 cwd: PathBuf,
321 command: Option<String>,
322 args: Vec<String>,
323 envs: HashMap<String, String>,
324 mut sender: mpsc::Sender<Result<u32>>,
325 window: &mut Window,
326 cx: &mut App,
327 ) -> Task<Result<()>> {
328 let terminal_task = self.workspace.update(cx, |workspace, cx| {
329 let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
330 anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
331 });
332
333 let terminal_panel = match terminal_panel {
334 Ok(panel) => panel,
335 Err(err) => return Task::ready(Err(err)),
336 };
337
338 terminal_panel.update(cx, |terminal_panel, cx| {
339 let terminal_task = terminal_panel.add_terminal(
340 TerminalKind::Debug {
341 command,
342 args,
343 envs,
344 cwd,
345 title,
346 },
347 task::RevealStrategy::Always,
348 window,
349 cx,
350 );
351
352 cx.spawn(|_, mut cx| async move {
353 let pid_task = async move {
354 let terminal = terminal_task.await?;
355
356 terminal.read_with(&mut cx, |terminal, _| terminal.pty_info.pid())
357 };
358
359 pid_task.await
360 })
361 })
362 });
363
364 cx.background_spawn(async move {
365 match terminal_task {
366 Ok(pid_task) => match pid_task.await {
367 Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
368 Ok(None) => {
369 sender
370 .send(Err(anyhow!(
371 "Terminal was spawned but PID was not available"
372 )))
373 .await?
374 }
375 Err(error) => sender.send(Err(anyhow!(error))).await?,
376 },
377 Err(error) => sender.send(Err(anyhow!(error))).await?,
378 };
379
380 Ok(())
381 })
382 }
383
384 fn handle_pane_event(
385 &mut self,
386 _: &Entity<Pane>,
387 event: &pane::Event,
388 window: &mut Window,
389 cx: &mut Context<Self>,
390 ) {
391 match event {
392 pane::Event::Remove { .. } => cx.emit(PanelEvent::Close),
393 pane::Event::ZoomIn => cx.emit(PanelEvent::ZoomIn),
394 pane::Event::ZoomOut => cx.emit(PanelEvent::ZoomOut),
395 pane::Event::AddItem { item } => {
396 self.workspace
397 .update(cx, |workspace, cx| {
398 item.added_to_pane(workspace, self.pane.clone(), window, cx)
399 })
400 .ok();
401 }
402 pane::Event::RemovedItem { item } => {
403 if let Some(debug_session) = item.downcast::<DebugSession>() {
404 debug_session.update(cx, |session, cx| {
405 session.shutdown(cx);
406 })
407 }
408 }
409 pane::Event::ActivateItem {
410 local: _,
411 focus_changed,
412 } => {
413 if *focus_changed {
414 if let Some(debug_session) = self
415 .pane
416 .read(cx)
417 .active_item()
418 .and_then(|item| item.downcast::<DebugSession>())
419 {
420 if let Some(running) = debug_session
421 .read_with(cx, |session, _| session.mode().as_running().cloned())
422 {
423 running.update(cx, |running, cx| {
424 running.go_to_selected_stack_frame(window, cx);
425 });
426 }
427 }
428 }
429 }
430
431 _ => {}
432 }
433 }
434}
435
436impl EventEmitter<PanelEvent> for DebugPanel {}
437impl EventEmitter<DebugPanelEvent> for DebugPanel {}
438impl EventEmitter<project::Event> for DebugPanel {}
439
440impl Focusable for DebugPanel {
441 fn focus_handle(&self, cx: &App) -> FocusHandle {
442 self.pane.focus_handle(cx)
443 }
444}
445
446impl Panel for DebugPanel {
447 fn pane(&self) -> Option<Entity<Pane>> {
448 Some(self.pane.clone())
449 }
450
451 fn persistent_name() -> &'static str {
452 "DebugPanel"
453 }
454
455 fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
456 DockPosition::Bottom
457 }
458
459 fn position_is_valid(&self, position: DockPosition) -> bool {
460 position == DockPosition::Bottom
461 }
462
463 fn set_position(
464 &mut self,
465 _position: DockPosition,
466 _window: &mut Window,
467 _cx: &mut Context<Self>,
468 ) {
469 }
470
471 fn size(&self, _window: &Window, _cx: &App) -> Pixels {
472 self.size
473 }
474
475 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
476 self.size = size.unwrap();
477 }
478
479 fn remote_id() -> Option<proto::PanelId> {
480 Some(proto::PanelId::DebugPanel)
481 }
482
483 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
484 Some(IconName::Debug)
485 }
486
487 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
488 if DebuggerSettings::get_global(cx).button {
489 Some("Debug Panel")
490 } else {
491 None
492 }
493 }
494
495 fn toggle_action(&self) -> Box<dyn Action> {
496 Box::new(ToggleFocus)
497 }
498
499 fn activation_priority(&self) -> u32 {
500 9
501 }
502 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
503 if active && self.pane.read(cx).items_len() == 0 {
504 let Some(project) = self.project.clone().upgrade() else {
505 return;
506 };
507 // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
508 self.pane.update(cx, |this, cx| {
509 this.add_item(
510 Box::new(DebugSession::inert(
511 project,
512 self.workspace.clone(),
513 window,
514 cx,
515 )),
516 false,
517 false,
518 None,
519 window,
520 cx,
521 );
522 });
523 }
524 }
525}
526
527impl Render for DebugPanel {
528 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
529 v_flex()
530 .key_context("DebugPanel")
531 .track_focus(&self.focus_handle(cx))
532 .size_full()
533 .child(self.pane.clone())
534 .into_any()
535 }
536}