debugger_ui.rs

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