debugger: Show run to cursor in editor's context menu (#35745)

Anthony Eid and Remco Smits created

This also fixed a bug where evaluate selected text was an available
option when the selected debug session was terminated.


Release Notes:

- debugger: add Run to Cursor back to Editor's context menu

Co-authored-by: Remco Smits <djsmits12@gmail.com>

Change summary

crates/debugger_ui/src/debugger_ui.rs   | 103 +++++++++++++++-----------
crates/editor/src/mouse_context_menu.rs |  16 ++-
crates/gpui/src/window.rs               |  19 ++++
3 files changed, 90 insertions(+), 48 deletions(-)

Detailed changes

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -299,59 +299,76 @@ pub fn init(cx: &mut App) {
                     else {
                         return;
                     };
+
+                    let session = active_session
+                        .read(cx)
+                        .running_state
+                        .read(cx)
+                        .session()
+                        .read(cx);
+
+                    if session.is_terminated() {
+                        return;
+                    }
+
                     let editor = cx.entity().downgrade();
-                    window.on_action(TypeId::of::<editor::actions::RunToCursor>(), {
-                        let editor = editor.clone();
-                        let active_session = active_session.clone();
-                        move |_, phase, _, cx| {
-                            if phase != DispatchPhase::Bubble {
-                                return;
-                            }
-                            maybe!({
-                                let (buffer, position, _) = editor
-                                    .update(cx, |editor, cx| {
-                                        let cursor_point: language::Point =
-                                            editor.selections.newest(cx).head();
 
-                                        editor
-                                            .buffer()
-                                            .read(cx)
-                                            .point_to_buffer_point(cursor_point, cx)
-                                    })
-                                    .ok()??;
+                    window.on_action_when(
+                        session.any_stopped_thread(),
+                        TypeId::of::<editor::actions::RunToCursor>(),
+                        {
+                            let editor = editor.clone();
+                            let active_session = active_session.clone();
+                            move |_, phase, _, cx| {
+                                if phase != DispatchPhase::Bubble {
+                                    return;
+                                }
+                                maybe!({
+                                    let (buffer, position, _) = editor
+                                        .update(cx, |editor, cx| {
+                                            let cursor_point: language::Point =
+                                                editor.selections.newest(cx).head();
+
+                                            editor
+                                                .buffer()
+                                                .read(cx)
+                                                .point_to_buffer_point(cursor_point, cx)
+                                        })
+                                        .ok()??;
 
-                                let path =
+                                    let path =
                                 debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
                                     &buffer, cx,
                                 )?;
 
-                                let source_breakpoint = SourceBreakpoint {
-                                    row: position.row,
-                                    path,
-                                    message: None,
-                                    condition: None,
-                                    hit_condition: None,
-                                    state: debugger::breakpoint_store::BreakpointState::Enabled,
-                                };
+                                    let source_breakpoint = SourceBreakpoint {
+                                        row: position.row,
+                                        path,
+                                        message: None,
+                                        condition: None,
+                                        hit_condition: None,
+                                        state: debugger::breakpoint_store::BreakpointState::Enabled,
+                                    };
 
-                                active_session.update(cx, |session, cx| {
-                                    session.running_state().update(cx, |state, cx| {
-                                        if let Some(thread_id) = state.selected_thread_id() {
-                                            state.session().update(cx, |session, cx| {
-                                                session.run_to_position(
-                                                    source_breakpoint,
-                                                    thread_id,
-                                                    cx,
-                                                );
-                                            })
-                                        }
+                                    active_session.update(cx, |session, cx| {
+                                        session.running_state().update(cx, |state, cx| {
+                                            if let Some(thread_id) = state.selected_thread_id() {
+                                                state.session().update(cx, |session, cx| {
+                                                    session.run_to_position(
+                                                        source_breakpoint,
+                                                        thread_id,
+                                                        cx,
+                                                    );
+                                                })
+                                            }
+                                        });
                                     });
-                                });
 
-                                Some(())
-                            });
-                        }
-                    });
+                                    Some(())
+                                });
+                            }
+                        },
+                    );
 
                     window.on_action(
                         TypeId::of::<editor::actions::EvaluateSelectedText>(),

crates/editor/src/mouse_context_menu.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
     EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
-    GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionEffects,
-    SelectionExt, ToDisplayPoint, ToggleCodeActions,
+    GoToTypeDefinition, Paste, Rename, RevealInFileManager, RunToCursor, SelectMode,
+    SelectionEffects, SelectionExt, ToDisplayPoint, ToggleCodeActions,
     actions::{Format, FormatSelections},
     selections_collection::SelectionsCollection,
 };
@@ -200,15 +200,21 @@ pub fn deploy_context_menu(
         });
 
         let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
+        let run_to_cursor = window.is_action_available(&RunToCursor, cx);
 
         ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
             let builder = menu
                 .on_blur_subscription(Subscription::new(|| {}))
+                .when(run_to_cursor, |builder| {
+                    builder.action("Run to Cursor", Box::new(RunToCursor))
+                })
                 .when(evaluate_selection && has_selections, |builder| {
-                    builder
-                        .action("Evaluate Selection", Box::new(EvaluateSelectedText))
-                        .separator()
+                    builder.action("Evaluate Selection", Box::new(EvaluateSelectedText))
                 })
+                .when(
+                    run_to_cursor || (evaluate_selection && has_selections),
+                    |builder| builder.separator(),
+                )
                 .action("Go to Definition", Box::new(GoToDefinition))
                 .action("Go to Declaration", Box::new(GoToDeclaration))
                 .action("Go to Type Definition", Box::new(GoToTypeDefinition))

crates/gpui/src/window.rs 🔗

@@ -4248,6 +4248,25 @@ impl Window {
             .on_action(action_type, Rc::new(listener));
     }
 
+    /// Register an action listener on the window for the next frame if the condition is true.
+    /// The type of action is determined by the first parameter of the given listener.
+    /// When the next frame is rendered the listener will be cleared.
+    ///
+    /// This is a fairly low-level method, so prefer using action handlers on elements unless you have
+    /// a specific need to register a global listener.
+    pub fn on_action_when(
+        &mut self,
+        condition: bool,
+        action_type: TypeId,
+        listener: impl Fn(&dyn Any, DispatchPhase, &mut Window, &mut App) + 'static,
+    ) {
+        if condition {
+            self.next_frame
+                .dispatch_tree
+                .on_action(action_type, Rc::new(listener));
+        }
+    }
+
     /// Read information about the GPU backing this window.
     /// Currently returns None on Mac and Windows.
     pub fn gpu_specs(&self) -> Option<GpuSpecs> {