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