debugger_ui.rs

  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
117pub fn init(cx: &mut App) {
118    DebuggerSettings::register(cx);
119    workspace::FollowableViewRegistry::register::<DebugSession>(cx);
120
121    cx.observe_new(|workspace: &mut Workspace, _, _| {
122        workspace
123            .register_action(spawn_task_or_modal)
124            .register_action(|workspace, _: &ToggleFocus, window, cx| {
125                workspace.toggle_panel_focus::<DebugPanel>(window, cx);
126            })
127            .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
128                NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
129            })
130            .register_action(|workspace: &mut Workspace, _: &Rerun, window, cx| {
131                let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
132                    return;
133                };
134
135                debug_panel.update(cx, |debug_panel, cx| {
136                    debug_panel.rerun_last_session(workspace, window, cx);
137                })
138            })
139            .register_action(
140                |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
141                    workspace.project().update(cx, |project, cx| {
142                        project.dap_store().update(cx, |store, cx| {
143                            store.shutdown_sessions(cx).detach();
144                        })
145                    })
146                },
147            )
148            .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
149                DebuggerOnboardingModal::toggle(workspace, window, cx)
150            })
151            .register_action_renderer(|div, workspace, _, cx| {
152                let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
153                    return div;
154                };
155                let Some(active_item) = debug_panel
156                    .read(cx)
157                    .active_session()
158                    .map(|session| session.read(cx).running_state().clone())
159                else {
160                    return div;
161                };
162                let running_state = active_item.read(cx);
163                if running_state.session().read(cx).is_terminated() {
164                    return div;
165                }
166
167                let caps = running_state.capabilities(cx);
168                let supports_step_back = caps.supports_step_back.unwrap_or_default();
169                let supports_detach = running_state.session().read(cx).is_attached();
170                let status = running_state.thread_status(cx);
171
172                let active_item = active_item.downgrade();
173                div.when(status == Some(ThreadStatus::Running), |div| {
174                    let active_item = active_item.clone();
175                    div.on_action(move |_: &Pause, _, cx| {
176                        active_item
177                            .update(cx, |item, cx| item.pause_thread(cx))
178                            .ok();
179                    })
180                })
181                .when(status == Some(ThreadStatus::Stopped), |div| {
182                    div.on_action({
183                        let active_item = active_item.clone();
184                        move |_: &StepInto, _, cx| {
185                            active_item.update(cx, |item, cx| item.step_in(cx)).ok();
186                        }
187                    })
188                    .on_action({
189                        let active_item = active_item.clone();
190                        move |_: &StepOver, _, cx| {
191                            active_item.update(cx, |item, cx| item.step_over(cx)).ok();
192                        }
193                    })
194                    .on_action({
195                        let active_item = active_item.clone();
196                        move |_: &StepOut, _, cx| {
197                            active_item.update(cx, |item, cx| item.step_out(cx)).ok();
198                        }
199                    })
200                    .when(supports_step_back, |div| {
201                        let active_item = active_item.clone();
202                        div.on_action(move |_: &StepBack, _, cx| {
203                            active_item.update(cx, |item, cx| item.step_back(cx)).ok();
204                        })
205                    })
206                    .on_action({
207                        let active_item = active_item.clone();
208                        move |_: &Continue, _, cx| {
209                            active_item
210                                .update(cx, |item, cx| item.continue_thread(cx))
211                                .ok();
212                        }
213                    })
214                    .on_action(cx.listener(
215                        |workspace, _: &ShowStackTrace, window, cx| {
216                            let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
217                                return;
218                            };
219
220                            if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
221                                let is_active = workspace
222                                    .active_item(cx)
223                                    .is_some_and(|item| item.item_id() == existing.item_id());
224                                workspace.activate_item(&existing, true, !is_active, window, cx);
225                            } else {
226                                let Some(active_session) = debug_panel.read(cx).active_session()
227                                else {
228                                    return;
229                                };
230
231                                let project = workspace.project();
232
233                                let stack_trace_view = active_session.update(cx, |session, cx| {
234                                    session.stack_trace_view(project, window, cx).clone()
235                                });
236
237                                workspace.add_item_to_active_pane(
238                                    Box::new(stack_trace_view),
239                                    None,
240                                    true,
241                                    window,
242                                    cx,
243                                );
244                            }
245                        },
246                    ))
247                })
248                .when(supports_detach, |div| {
249                    let active_item = active_item.clone();
250                    div.on_action(move |_: &Detach, _, cx| {
251                        active_item
252                            .update(cx, |item, cx| item.detach_client(cx))
253                            .ok();
254                    })
255                })
256                .on_action({
257                    let active_item = active_item.clone();
258                    move |_: &Restart, _, cx| {
259                        active_item
260                            .update(cx, |item, cx| item.restart_session(cx))
261                            .ok();
262                    }
263                })
264                .on_action({
265                    let active_item = active_item.clone();
266                    move |_: &RerunSession, window, cx| {
267                        active_item
268                            .update(cx, |item, cx| item.rerun_session(window, cx))
269                            .ok();
270                    }
271                })
272                .on_action({
273                    let active_item = active_item.clone();
274                    move |_: &Stop, _, cx| {
275                        active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
276                    }
277                })
278                .on_action({
279                    let active_item = active_item.clone();
280                    move |_: &ToggleIgnoreBreakpoints, _, cx| {
281                        active_item
282                            .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
283                            .ok();
284                    }
285                })
286                .on_action(move |_: &ToggleUserFrames, _, cx| {
287                    if let Some((thread_status, stack_frame_list)) = active_item
288                        .read_with(cx, |item, cx| {
289                            (item.thread_status(cx), item.stack_frame_list().clone())
290                        })
291                        .ok()
292                    {
293                        stack_frame_list.update(cx, |stack_frame_list, cx| {
294                            stack_frame_list.toggle_frame_filter(thread_status, cx);
295                        })
296                    }
297                })
298            });
299    })
300    .detach();
301
302    cx.observe_new({
303        move |editor: &mut Editor, _, _| {
304            editor
305                .register_action_renderer(move |editor, window, cx| {
306                    let Some(workspace) = editor.workspace() else {
307                        return;
308                    };
309                    let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
310                        return;
311                    };
312                    let Some(active_session) =
313                        debug_panel.update(cx, |panel, _| panel.active_session())
314                    else {
315                        return;
316                    };
317
318                    let session = active_session
319                        .read(cx)
320                        .running_state
321                        .read(cx)
322                        .session()
323                        .read(cx);
324
325                    if session.is_terminated() {
326                        return;
327                    }
328
329                    let editor = cx.entity().downgrade();
330
331                    window.on_action_when(
332                        session.any_stopped_thread(),
333                        TypeId::of::<editor::actions::RunToCursor>(),
334                        {
335                            let editor = editor.clone();
336                            let active_session = active_session.clone();
337                            move |_, phase, _, cx| {
338                                if phase != DispatchPhase::Bubble {
339                                    return;
340                                }
341                                maybe!({
342                                    let (buffer, position, _) = editor
343                                        .update(cx, |editor, cx| {
344                                            let cursor_point: language::Point = editor
345                                                .selections
346                                                .newest(&editor.display_snapshot(cx))
347                                                .head();
348
349                                            editor
350                                                .buffer()
351                                                .read(cx)
352                                                .point_to_buffer_point(cursor_point, cx)
353                                        })
354                                        .ok()??;
355
356                                    let path =
357                                debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
358                                    &buffer, cx,
359                                )?;
360
361                                    let source_breakpoint = SourceBreakpoint {
362                                        row: position.row,
363                                        path,
364                                        message: None,
365                                        condition: None,
366                                        hit_condition: None,
367                                        state: debugger::breakpoint_store::BreakpointState::Enabled,
368                                    };
369
370                                    active_session.update(cx, |session, cx| {
371                                        session.running_state().update(cx, |state, cx| {
372                                            if let Some(thread_id) = state.selected_thread_id() {
373                                                state.session().update(cx, |session, cx| {
374                                                    session.run_to_position(
375                                                        source_breakpoint,
376                                                        thread_id,
377                                                        cx,
378                                                    );
379                                                })
380                                            }
381                                        });
382                                    });
383
384                                    Some(())
385                                });
386                            }
387                        },
388                    );
389
390                    window.on_action(
391                        TypeId::of::<editor::actions::EvaluateSelectedText>(),
392                        move |_, _, window, cx| {
393                            maybe!({
394                                let text = editor
395                                    .update(cx, |editor, cx| {
396                                        editor.text_for_range(
397                                            editor
398                                                .selections
399                                                .newest(&editor.display_snapshot(cx))
400                                                .range(),
401                                            &mut None,
402                                            window,
403                                            cx,
404                                        )
405                                    })
406                                    .ok()??;
407
408                                active_session.update(cx, |session, cx| {
409                                    session.running_state().update(cx, |state, cx| {
410                                        let stack_id = state.selected_stack_frame_id(cx);
411
412                                        state.session().update(cx, |session, cx| {
413                                            session
414                                                .evaluate(text, None, stack_id, None, cx)
415                                                .detach();
416                                        });
417                                    });
418                                });
419
420                                Some(())
421                            });
422                        },
423                    );
424                })
425                .detach();
426        }
427    })
428    .detach();
429}
430
431fn spawn_task_or_modal(
432    workspace: &mut Workspace,
433    action: &Spawn,
434    window: &mut ui::Window,
435    cx: &mut ui::Context<Workspace>,
436) {
437    match action {
438        Spawn::ByName {
439            task_name,
440            reveal_target,
441        } => {
442            let overrides = reveal_target.map(|reveal_target| TaskOverrides {
443                reveal_target: Some(reveal_target),
444            });
445            let name = task_name.clone();
446            tasks_ui::spawn_tasks_filtered(
447                move |(_, task)| task.label.eq(&name),
448                overrides,
449                window,
450                cx,
451            )
452            .detach_and_log_err(cx)
453        }
454        Spawn::ByTag {
455            task_tag,
456            reveal_target,
457        } => {
458            let overrides = reveal_target.map(|reveal_target| TaskOverrides {
459                reveal_target: Some(reveal_target),
460            });
461            let tag = task_tag.clone();
462            tasks_ui::spawn_tasks_filtered(
463                move |(_, task)| task.tags.contains(&tag),
464                overrides,
465                window,
466                cx,
467            )
468            .detach_and_log_err(cx)
469        }
470        Spawn::ViaModal { reveal_target } => {
471            NewProcessModal::show(workspace, window, NewProcessMode::Task, *reveal_target, cx);
472        }
473    }
474}