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