debugger_ui.rs

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