1use std::any::TypeId;
2
3use debugger_panel::DebugPanel;
4use editor::{Editor, MultiBufferOffsetUtf16};
5use gpui::{Action, App, DispatchPhase, EntityInputHandler, actions};
6use new_process_modal::{NewProcessModal, NewProcessMode};
7use onboarding_modal::DebuggerOnboardingModal;
8use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
9use schemars::JsonSchema;
10use serde::Deserialize;
11use session::DebugSession;
12use stack_trace_view::StackTraceView;
13use tasks_ui::{Spawn, TaskOverrides};
14use ui::{FluentBuilder, InteractiveElement};
15use util::maybe;
16use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
17use zed_actions::debugger::OpenOnboardingModal;
18use zed_actions::{Toggle, ToggleFocus};
19
20pub mod attach_modal;
21pub mod debugger_panel;
22mod dropdown_menus;
23mod new_process_modal;
24mod onboarding_modal;
25mod persistence;
26pub(crate) mod session;
27mod stack_trace_view;
28
29#[cfg(any(test, feature = "test-support"))]
30pub mod tests;
31
32// Let's see the diff-test in action.
33actions!(
34 debugger,
35 [
36 /// Starts a new debugging session.
37 Start,
38 /// Continues execution until the next breakpoint.
39 Continue,
40 /// Detaches the debugger from the running process.
41 Detach,
42 /// Pauses the currently running program.
43 Pause,
44 /// Restarts the current debugging session.
45 Restart,
46 /// Reruns the current debugging session with the same configuration.
47 RerunSession,
48 /// Steps into the next function call.
49 StepInto,
50 /// Steps over the current line.
51 StepOver,
52 /// Steps out of the current function.
53 StepOut,
54 /// Steps back to the previous statement.
55 StepBack,
56 /// Stops the debugging session.
57 Stop,
58 /// Toggles whether to ignore all breakpoints.
59 ToggleIgnoreBreakpoints,
60 /// Clears all breakpoints in the project.
61 ClearAllBreakpoints,
62 /// Focuses on the debugger console panel.
63 FocusConsole,
64 /// Focuses on the variables panel.
65 FocusVariables,
66 /// Focuses on the breakpoint list panel.
67 FocusBreakpointList,
68 /// Focuses on the call stack frames panel.
69 FocusFrames,
70 /// Focuses on the loaded modules panel.
71 FocusModules,
72 /// Focuses on the loaded sources panel.
73 FocusLoadedSources,
74 /// Focuses on the terminal panel.
75 FocusTerminal,
76 /// Shows the stack trace for the current thread.
77 ShowStackTrace,
78 /// Toggles the thread picker dropdown.
79 ToggleThreadPicker,
80 /// Toggles the session picker dropdown.
81 ToggleSessionPicker,
82 /// Reruns the last debugging session.
83 #[action(deprecated_aliases = ["debugger::RerunLastSession"])]
84 Rerun,
85 /// Toggles expansion of the selected item in the debugger UI.
86 ToggleExpandItem,
87 /// Toggle the user frame filter in the stack frame list
88 /// When toggled on, only frames from the user's code are shown
89 /// When toggled off, all frames are shown
90 ToggleUserFrames,
91 ]
92);
93
94/// Set a data breakpoint on the selected variable or memory region.
95#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)]
96#[action(namespace = debugger)]
97#[serde(deny_unknown_fields)]
98pub struct ToggleDataBreakpoint {
99 /// The type of data breakpoint
100 /// Read & Write
101 /// Read
102 /// Write
103 #[serde(default)]
104 pub access_type: Option<dap::DataBreakpointAccessType>,
105}
106
107actions!(
108 dev,
109 [
110 /// Copies debug adapter launch arguments to clipboard.
111 CopyDebugAdapterArguments
112 ]
113);
114
115pub fn init(cx: &mut App) {
116 workspace::FollowableViewRegistry::register::<DebugSession>(cx);
117
118 cx.observe_new(|workspace: &mut Workspace, _, _| {
119 workspace
120 .register_action(spawn_task_or_modal)
121 .register_action(|workspace, _: &ToggleFocus, window, cx| {
122 workspace.toggle_panel_focus::<DebugPanel>(window, cx);
123 })
124 .register_action(|workspace, _: &Toggle, window, cx| {
125 if !workspace.toggle_panel_focus::<DebugPanel>(window, cx) {
126 workspace.close_panel::<DebugPanel>(window, cx);
127 }
128 })
129 .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
130 NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
131 })
132 .register_action(|workspace: &mut Workspace, _: &Rerun, window, cx| {
133 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
134 return;
135 };
136
137 debug_panel.update(cx, |debug_panel, cx| {
138 debug_panel.rerun_last_session(workspace, window, cx);
139 })
140 })
141 .register_action(
142 |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
143 workspace.project().update(cx, |project, cx| {
144 project.dap_store().update(cx, |store, cx| {
145 store.shutdown_sessions(cx).detach();
146 })
147 })
148 },
149 )
150 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
151 DebuggerOnboardingModal::toggle(workspace, window, cx)
152 })
153 .register_action_renderer(|div, workspace, _, cx| {
154 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
155 return div;
156 };
157 let Some(active_item) = debug_panel
158 .read(cx)
159 .active_session()
160 .map(|session| session.read(cx).running_state().clone())
161 else {
162 return div;
163 };
164 let running_state = active_item.read(cx);
165 if running_state.session().read(cx).is_terminated() {
166 return div;
167 }
168
169 let caps = running_state.capabilities(cx);
170 let supports_step_back = caps.supports_step_back.unwrap_or_default();
171 let supports_detach = running_state.session().read(cx).is_attached();
172 let status = running_state.thread_status(cx);
173
174 let active_item = active_item.downgrade();
175 div.when(status == Some(ThreadStatus::Running), |div| {
176 let active_item = active_item.clone();
177 div.on_action(move |_: &Pause, _, cx| {
178 active_item
179 .update(cx, |item, cx| item.pause_thread(cx))
180 .ok();
181 })
182 })
183 .when(status == Some(ThreadStatus::Stopped), |div| {
184 div.on_action({
185 let active_item = active_item.clone();
186 move |_: &StepInto, _, cx| {
187 active_item.update(cx, |item, cx| item.step_in(cx)).ok();
188 }
189 })
190 .on_action({
191 let active_item = active_item.clone();
192 move |_: &StepOver, _, cx| {
193 active_item.update(cx, |item, cx| item.step_over(cx)).ok();
194 }
195 })
196 .on_action({
197 let active_item = active_item.clone();
198 move |_: &StepOut, _, cx| {
199 active_item.update(cx, |item, cx| item.step_out(cx)).ok();
200 }
201 })
202 .when(supports_step_back, |div| {
203 let active_item = active_item.clone();
204 div.on_action(move |_: &StepBack, _, cx| {
205 active_item.update(cx, |item, cx| item.step_back(cx)).ok();
206 })
207 })
208 .on_action({
209 let active_item = active_item.clone();
210 move |_: &Continue, _, cx| {
211 active_item
212 .update(cx, |item, cx| item.continue_thread(cx))
213 .ok();
214 }
215 })
216 .on_action(cx.listener(
217 |workspace, _: &ShowStackTrace, window, cx| {
218 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
219 return;
220 };
221
222 if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
223 let is_active = workspace
224 .active_item(cx)
225 .is_some_and(|item| item.item_id() == existing.item_id());
226 workspace.activate_item(&existing, true, !is_active, window, cx);
227 } else {
228 let Some(active_session) = debug_panel.read(cx).active_session()
229 else {
230 return;
231 };
232
233 let project = workspace.project();
234
235 let stack_trace_view = active_session.update(cx, |session, cx| {
236 session.stack_trace_view(project, window, cx).clone()
237 });
238
239 workspace.add_item_to_active_pane(
240 Box::new(stack_trace_view),
241 None,
242 true,
243 window,
244 cx,
245 );
246 }
247 },
248 ))
249 })
250 .when(supports_detach, |div| {
251 let active_item = active_item.clone();
252 div.on_action(move |_: &Detach, _, cx| {
253 active_item
254 .update(cx, |item, cx| item.detach_client(cx))
255 .ok();
256 })
257 })
258 .on_action({
259 let active_item = active_item.clone();
260 move |_: &Restart, _, cx| {
261 active_item
262 .update(cx, |item, cx| item.restart_session(cx))
263 .ok();
264 }
265 })
266 .on_action({
267 let active_item = active_item.clone();
268 move |_: &RerunSession, window, cx| {
269 active_item
270 .update(cx, |item, cx| item.rerun_session(window, cx))
271 .ok();
272 }
273 })
274 .on_action({
275 let active_item = active_item.clone();
276 move |_: &Stop, _, cx| {
277 active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
278 }
279 })
280 .on_action({
281 let active_item = active_item.clone();
282 move |_: &ToggleIgnoreBreakpoints, _, cx| {
283 active_item
284 .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
285 .ok();
286 }
287 })
288 .on_action(move |_: &ToggleUserFrames, _, cx| {
289 if let Some((thread_status, stack_frame_list)) = active_item
290 .read_with(cx, |item, cx| {
291 (item.thread_status(cx), item.stack_frame_list().clone())
292 })
293 .ok()
294 {
295 stack_frame_list.update(cx, |stack_frame_list, cx| {
296 stack_frame_list.toggle_frame_filter(thread_status, cx);
297 })
298 }
299 })
300 });
301 })
302 .detach();
303
304 cx.observe_new({
305 move |editor: &mut Editor, _, _| {
306 editor
307 .register_action_renderer(move |editor, window, cx| {
308 let Some(workspace) = editor.workspace() else {
309 return;
310 };
311 let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
312 return;
313 };
314 let Some(active_session) =
315 debug_panel.update(cx, |panel, _| panel.active_session())
316 else {
317 return;
318 };
319
320 let session = active_session
321 .read(cx)
322 .running_state
323 .read(cx)
324 .session()
325 .read(cx);
326
327 if session.is_terminated() {
328 return;
329 }
330
331 let editor = cx.entity().downgrade();
332
333 window.on_action_when(
334 session.any_stopped_thread(),
335 TypeId::of::<editor::actions::RunToCursor>(),
336 {
337 let editor = editor.clone();
338 let active_session = active_session.clone();
339 move |_, phase, _, cx| {
340 if phase != DispatchPhase::Bubble {
341 return;
342 }
343 maybe!({
344 let (buffer, position, _) = editor
345 .update(cx, |editor, cx| {
346 let cursor_point: language::Point = editor
347 .selections
348 .newest(&editor.display_snapshot(cx))
349 .head();
350
351 editor
352 .buffer()
353 .read(cx)
354 .point_to_buffer_point(cursor_point, cx)
355 })
356 .ok()??;
357
358 let path =
359 debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
360 &buffer, cx,
361 )?;
362
363 let source_breakpoint = SourceBreakpoint {
364 row: position.row,
365 path,
366 message: None,
367 condition: None,
368 hit_condition: None,
369 state: debugger::breakpoint_store::BreakpointState::Enabled,
370 };
371
372 active_session.update(cx, |session, cx| {
373 session.running_state().update(cx, |state, cx| {
374 if let Some(thread_id) = state.selected_thread_id() {
375 state.session().update(cx, |session, cx| {
376 session.run_to_position(
377 source_breakpoint,
378 thread_id,
379 cx,
380 );
381 })
382 }
383 });
384 });
385
386 Some(())
387 });
388 }
389 },
390 );
391
392 window.on_action(
393 TypeId::of::<editor::actions::EvaluateSelectedText>(),
394 move |_, _, window, cx| {
395 let status = maybe!({
396 let text = editor
397 .update(cx, |editor, cx| {
398 let range = editor
399 .selections
400 .newest::<MultiBufferOffsetUtf16>(
401 &editor.display_snapshot(cx),
402 )
403 .range();
404 editor.text_for_range(
405 range.start.0.0..range.end.0.0,
406 &mut None,
407 window,
408 cx,
409 )
410 })
411 .ok()??;
412
413 active_session.update(cx, |session, cx| {
414 session.running_state().update(cx, |state, cx| {
415 let stack_id = state.selected_stack_frame_id(cx);
416
417 state.session().update(cx, |session, cx| {
418 session
419 .evaluate(
420 text,
421 Some(dap::EvaluateArgumentsContext::Repl),
422 stack_id,
423 None,
424 cx,
425 )
426 .detach();
427 });
428 });
429 });
430
431 Some(())
432 });
433 if status.is_some() {
434 cx.stop_propagation();
435 }
436 },
437 );
438 })
439 .detach();
440 }
441 })
442 .detach();
443}
444
445fn spawn_task_or_modal(
446 workspace: &mut Workspace,
447 action: &Spawn,
448 window: &mut ui::Window,
449 cx: &mut ui::Context<Workspace>,
450) {
451 match action {
452 Spawn::ByName {
453 task_name,
454 reveal_target,
455 } => {
456 let overrides = reveal_target.map(|reveal_target| TaskOverrides {
457 reveal_target: Some(reveal_target),
458 });
459 let name = task_name.clone();
460 tasks_ui::spawn_tasks_filtered(
461 move |(_, task)| task.label.eq(&name),
462 overrides,
463 window,
464 cx,
465 )
466 .detach_and_log_err(cx)
467 }
468 Spawn::ByTag {
469 task_tag,
470 reveal_target,
471 } => {
472 let overrides = reveal_target.map(|reveal_target| TaskOverrides {
473 reveal_target: Some(reveal_target),
474 });
475 let tag = task_tag.clone();
476 tasks_ui::spawn_tasks_filtered(
477 move |(_, task)| task.tags.contains(&tag),
478 overrides,
479 window,
480 cx,
481 )
482 .detach_and_log_err(cx)
483 }
484 Spawn::ViaModal { reveal_target } => {
485 NewProcessModal::show(workspace, window, NewProcessMode::Task, *reveal_target, cx);
486 }
487 }
488}