debugger_ui.rs

  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}