debugger: Add run to cursor and evaluate selected text actions (#28405)

Anthony Eid , Max Brunsfeld , and Remco Smits created

## Summary

### Actions

This PR implements actions that allow a user to "run to cursor" and
"evaluate selected text" while there's an active debug session and
exposes the functionality to the UI as well.

- Run to cursor: Can be accessed by right clicking on the gutter
- Evaluate selected text: Can be accessed by selecting text then right
clicking in the editor

### Bug fixes

I also fixed these bugs as well

- Panic when using debugger: Stop action
- Debugger actions command palette filter not working properly in all
cases
- We stopped displaying the correct label in the session's context menu
when a session was terminated

Release Notes:

- N/A

---------

Co-authored-by: Max Brunsfeld <max@zed.dev>
Co-authored-by: Remco Smits <djsmits12@gmail.com>

Change summary

Cargo.lock                                                 |   1 
crates/debugger_ui/src/debugger_panel.rs                   | 159 +++++--
crates/debugger_ui/src/debugger_ui.rs                      |  96 ++++
crates/debugger_ui/src/session.rs                          |  39 +
crates/debugger_ui/src/session/running.rs                  |   5 
crates/debugger_ui/src/session/running/console.rs          |   4 
crates/debugger_ui/src/session/running/stack_frame_list.rs |  18 
crates/debugger_ui/src/tests/stack_frame_list.rs           |   6 
crates/debugger_ui/src/tests/variable_list.rs              |  12 
crates/editor/Cargo.toml                                   |   1 
crates/editor/src/actions.rs                               |   2 
crates/editor/src/editor.rs                                |  18 
crates/editor/src/mouse_context_menu.rs                    |  22 
crates/project/src/debugger/breakpoint_store.rs            |   2 
crates/project/src/debugger/session.rs                     |  51 ++
15 files changed, 334 insertions(+), 102 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4600,6 +4600,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
+ "command_palette_hooks",
  "convert_case 0.8.0",
  "ctor",
  "db",

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -15,6 +15,7 @@ use gpui::{
     Action, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, FocusHandle,
     Focusable, Subscription, Task, WeakEntity, actions,
 };
+
 use project::{
     Project,
     debugger::{
@@ -94,6 +95,87 @@ impl DebugPanel {
         })
     }
 
+    fn filter_action_types(&self, cx: &mut App) {
+        let (has_active_session, supports_restart, support_step_back, status) = self
+            .active_session()
+            .map(|item| {
+                let running = item.read(cx).mode().as_running().cloned();
+
+                match running {
+                    Some(running) => {
+                        let caps = running.read(cx).capabilities(cx);
+                        (
+                            !running.read(cx).session().read(cx).is_terminated(),
+                            caps.supports_restart_request.unwrap_or_default(),
+                            caps.supports_step_back.unwrap_or_default(),
+                            running.read(cx).thread_status(cx),
+                        )
+                    }
+                    None => (false, false, false, None),
+                }
+            })
+            .unwrap_or((false, false, false, None));
+
+        let filter = CommandPaletteFilter::global_mut(cx);
+        let debugger_action_types = [
+            TypeId::of::<Disconnect>(),
+            TypeId::of::<Stop>(),
+            TypeId::of::<ToggleIgnoreBreakpoints>(),
+        ];
+
+        let running_action_types = [TypeId::of::<Pause>()];
+
+        let stopped_action_type = [
+            TypeId::of::<Continue>(),
+            TypeId::of::<StepOver>(),
+            TypeId::of::<StepInto>(),
+            TypeId::of::<StepOut>(),
+            TypeId::of::<editor::actions::DebuggerRunToCursor>(),
+            TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
+        ];
+
+        let step_back_action_type = [TypeId::of::<StepBack>()];
+        let restart_action_type = [TypeId::of::<Restart>()];
+
+        if has_active_session {
+            filter.show_action_types(debugger_action_types.iter());
+
+            if supports_restart {
+                filter.show_action_types(restart_action_type.iter());
+            } else {
+                filter.hide_action_types(&restart_action_type);
+            }
+
+            if support_step_back {
+                filter.show_action_types(step_back_action_type.iter());
+            } else {
+                filter.hide_action_types(&step_back_action_type);
+            }
+
+            match status {
+                Some(ThreadStatus::Running) => {
+                    filter.show_action_types(running_action_types.iter());
+                    filter.hide_action_types(&stopped_action_type);
+                }
+                Some(ThreadStatus::Stopped) => {
+                    filter.show_action_types(stopped_action_type.iter());
+                    filter.hide_action_types(&running_action_types);
+                }
+                _ => {
+                    filter.hide_action_types(&running_action_types);
+                    filter.hide_action_types(&stopped_action_type);
+                }
+            }
+        } else {
+            // show only the `debug: start`
+            filter.hide_action_types(&debugger_action_types);
+            filter.hide_action_types(&step_back_action_type);
+            filter.hide_action_types(&restart_action_type);
+            filter.hide_action_types(&running_action_types);
+            filter.hide_action_types(&stopped_action_type);
+        }
+    }
+
     pub fn load(
         workspace: WeakEntity<Workspace>,
         cx: AsyncWindowContext,
@@ -111,63 +193,15 @@ impl DebugPanel {
                     )
                 });
 
+                cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
+                    Self::filter_action_types(debug_panel, cx);
+                })
+                .detach();
+
                 cx.observe(&debug_panel, |_, debug_panel, cx| {
-                    let (has_active_session, supports_restart, support_step_back) = debug_panel
-                        .update(cx, |this, cx| {
-                            this.active_session()
-                                .map(|item| {
-                                    let running = item.read(cx).mode().as_running().cloned();
-
-                                    match running {
-                                        Some(running) => {
-                                            let caps = running.read(cx).capabilities(cx);
-                                            (
-                                                true,
-                                                caps.supports_restart_request.unwrap_or_default(),
-                                                caps.supports_step_back.unwrap_or_default(),
-                                            )
-                                        }
-                                        None => (false, false, false),
-                                    }
-                                })
-                                .unwrap_or((false, false, false))
-                        });
-
-                    let filter = CommandPaletteFilter::global_mut(cx);
-                    let debugger_action_types = [
-                        TypeId::of::<Continue>(),
-                        TypeId::of::<StepOver>(),
-                        TypeId::of::<StepInto>(),
-                        TypeId::of::<StepOut>(),
-                        TypeId::of::<Stop>(),
-                        TypeId::of::<Disconnect>(),
-                        TypeId::of::<Pause>(),
-                        TypeId::of::<ToggleIgnoreBreakpoints>(),
-                    ];
-
-                    let step_back_action_type = [TypeId::of::<StepBack>()];
-                    let restart_action_type = [TypeId::of::<Restart>()];
-
-                    if has_active_session {
-                        filter.show_action_types(debugger_action_types.iter());
-
-                        if supports_restart {
-                            filter.show_action_types(restart_action_type.iter());
-                        } else {
-                            filter.hide_action_types(&restart_action_type);
-                        }
-
-                        if support_step_back {
-                            filter.show_action_types(step_back_action_type.iter());
-                        } else {
-                            filter.hide_action_types(&step_back_action_type);
-                        }
-                    } else {
-                        // show only the `debug: start`
-                        filter.hide_action_types(&debugger_action_types);
-                        filter.hide_action_types(&step_back_action_type);
-                        filter.hide_action_types(&restart_action_type);
-                    }
+                    debug_panel.update(cx, |debug_panel, cx| {
+                        Self::filter_action_types(debug_panel, cx);
+                    });
                 })
                 .detach();
 
@@ -243,6 +277,12 @@ impl DebugPanel {
                     cx,
                 );
 
+                if let Some(running) = session_item.read(cx).mode().as_running().cloned() {
+                    // We might want to make this an event subscription and only notify when a new thread is selected
+                    // This is used to filter the command menu correctly
+                    cx.observe(&running, |_, _, cx| cx.notify()).detach();
+                }
+
                 self.sessions.push(session_item.clone());
                 self.activate_session(session_item, window, cx);
             }
@@ -360,6 +400,8 @@ impl DebugPanel {
                 self.active_session = self.sessions.first().cloned();
             }
         }
+
+        cx.notify();
     }
 
     fn sessions_drop_down_menu(
@@ -378,7 +420,7 @@ impl DebugPanel {
             ContextMenu::build(window, cx, move |mut this, _, _| {
                 for session in sessions.into_iter() {
                     let weak_session = session.downgrade();
-                    let weak_id = weak_session.entity_id();
+                    let weak_session_id = weak_session.entity_id();
 
                     this = this.custom_entry(
                         {
@@ -400,7 +442,8 @@ impl DebugPanel {
                                                     let weak = weak.clone();
                                                     move |_, _, cx| {
                                                         weak.update(cx, |panel, cx| {
-                                                            panel.close_session(weak_id, cx);
+                                                            panel
+                                                                .close_session(weak_session_id, cx);
                                                         })
                                                         .ok();
                                                     }

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -1,10 +1,13 @@
 use dap::debugger_settings::DebuggerSettings;
 use debugger_panel::{DebugPanel, ToggleFocus};
+use editor::Editor;
 use feature_flags::{Debugger, FeatureFlagViewExt};
-use gpui::{App, actions};
+use gpui::{App, EntityInputHandler, actions};
 use new_session_modal::NewSessionModal;
+use project::debugger::{self, breakpoint_store::SourceBreakpoint};
 use session::DebugSession;
 use settings::Settings;
+use util::maybe;
 use workspace::{ShutdownDebugAdapters, Workspace};
 
 pub mod attach_modal;
@@ -110,7 +113,9 @@ pub fn init(cx: &mut App) {
                                 .active_session()
                                 .and_then(|session| session.read(cx).mode().as_running().cloned())
                         }) {
-                            active_item.update(cx, |item, cx| item.stop_thread(cx))
+                            cx.defer(move |cx| {
+                                active_item.update(cx, |item, cx| item.stop_thread(cx))
+                            })
                         }
                     }
                 })
@@ -155,4 +160,91 @@ pub fn init(cx: &mut App) {
         })
     })
     .detach();
+
+    cx.observe_new({
+        move |editor: &mut Editor, _, cx| {
+            editor
+                .register_action(cx.listener(
+                    move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {
+                        maybe!({
+                            let debug_panel =
+                                editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
+                            let cursor_point: language::Point = editor.selections.newest(cx).head();
+                            let active_session = debug_panel.read(cx).active_session()?;
+
+                            let (buffer, position, _) = editor
+                                .buffer()
+                                .read(cx)
+                                .point_to_buffer_point(cursor_point, cx)?;
+
+                            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,
+                            };
+
+                            active_session
+                                .update(cx, |session_item, _| {
+                                    session_item.mode().as_running().cloned()
+                                })?
+                                .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(())
+                        });
+                    },
+                ))
+                .detach();
+
+            editor
+                .register_action(cx.listener(
+                    move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| {
+                        maybe!({
+                            let debug_panel =
+                                editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
+                            let active_session = debug_panel.read(cx).active_session()?;
+
+                            let text = editor.text_for_range(
+                                editor.selections.newest(cx).range(),
+                                &mut None,
+                                window,
+                                cx,
+                            )?;
+
+                            active_session
+                                .update(cx, |session_item, _| {
+                                    session_item.mode().as_running().cloned()
+                                })?
+                                .update(cx, |state, cx| {
+                                    let stack_id = state.selected_stack_frame_id(cx);
+
+                                    state.session().update(cx, |session, cx| {
+                                        session.evaluate(text, None, stack_id, None, cx);
+                                    })
+                                });
+                            Some(())
+                        });
+                    },
+                ))
+                .detach();
+        }
+    })
+    .detach();
 }

crates/debugger_ui/src/session.rs 🔗

@@ -1,5 +1,7 @@
 pub mod running;
 
+use std::sync::OnceLock;
+
 use dap::client::SessionId;
 use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
 use project::Project;
@@ -30,6 +32,7 @@ impl DebugSessionState {
 pub struct DebugSession {
     remote_id: Option<workspace::ViewId>,
     mode: DebugSessionState,
+    label: OnceLock<String>,
     dap_store: WeakEntity<DapStore>,
     _debug_panel: WeakEntity<DebugPanel>,
     _worktree_store: WeakEntity<WorktreeStore>,
@@ -68,6 +71,7 @@ impl DebugSession {
             })],
             remote_id: None,
             mode: DebugSessionState::Running(mode),
+            label: OnceLock::new(),
             dap_store: project.read(cx).dap_store().downgrade(),
             _debug_panel,
             _worktree_store: project.read(cx).worktree_store().downgrade(),
@@ -92,36 +96,45 @@ impl DebugSession {
     }
 
     pub(crate) fn label(&self, cx: &App) -> String {
+        if let Some(label) = self.label.get() {
+            return label.to_owned();
+        }
+
         let session_id = match &self.mode {
             DebugSessionState::Running(running_state) => running_state.read(cx).session_id(),
         };
+
         let Ok(Some(session)) = self
             .dap_store
             .read_with(cx, |store, _| store.session_by_id(session_id))
         else {
             return "".to_owned();
         };
-        session
-            .read(cx)
-            .as_local()
-            .expect("Remote Debug Sessions are not implemented yet")
-            .label()
+
+        self.label
+            .get_or_init(|| {
+                session
+                    .read(cx)
+                    .as_local()
+                    .expect("Remote Debug Sessions are not implemented yet")
+                    .label()
+            })
+            .to_owned()
     }
 
     pub(crate) fn label_element(&self, cx: &App) -> AnyElement {
         let label = self.label(cx);
 
-        let (icon, color) = match &self.mode {
+        let icon = match &self.mode {
             DebugSessionState::Running(state) => {
                 if state.read(cx).session().read(cx).is_terminated() {
-                    (Some(Indicator::dot().color(Color::Error)), Color::Error)
+                    Some(Indicator::dot().color(Color::Error))
                 } else {
                     match state.read(cx).thread_status(cx).unwrap_or_default() {
-                        project::debugger::session::ThreadStatus::Stopped => (
-                            Some(Indicator::dot().color(Color::Conflict)),
-                            Color::Conflict,
-                        ),
-                        _ => (Some(Indicator::dot().color(Color::Success)), Color::Success),
+                        project::debugger::session::ThreadStatus::Stopped => {
+                            Some(Indicator::dot().color(Color::Conflict))
+                        }
+                        _ => Some(Indicator::dot().color(Color::Success)),
                     }
                 }
             }
@@ -131,7 +144,7 @@ impl DebugSession {
             .gap_2()
             .when_some(icon, |this, indicator| this.child(indicator))
             .justify_between()
-            .child(Label::new(label).color(color))
+            .child(Label::new(label))
             .into_any_element()
     }
 }

crates/debugger_ui/src/session/running.rs 🔗

@@ -432,6 +432,10 @@ impl RunningState {
         self.session_id
     }
 
+    pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
+        self.stack_frame_list.read(cx).selected_stack_frame_id()
+    }
+
     #[cfg(test)]
     pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
         &self.stack_frame_list
@@ -492,7 +496,6 @@ impl RunningState {
         }
     }
 
-    #[cfg(test)]
     pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
         self.thread_id
     }

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -141,7 +141,7 @@ impl Console {
             state.evaluate(
                 expression,
                 Some(dap::EvaluateArgumentsContext::Variables),
-                self.stack_frame_list.read(cx).current_stack_frame_id(),
+                self.stack_frame_list.read(cx).selected_stack_frame_id(),
                 None,
                 cx,
             );
@@ -384,7 +384,7 @@ impl ConsoleQueryBarCompletionProvider {
     ) -> Task<Result<Option<Vec<Completion>>>> {
         let completion_task = console.update(cx, |console, cx| {
             console.session.update(cx, |state, cx| {
-                let frame_id = console.stack_frame_list.read(cx).current_stack_frame_id();
+                let frame_id = console.stack_frame_list.read(cx).selected_stack_frame_id();
 
                 state.completions(
                     CompletionsQuery::new(buffer.read(cx), buffer_position, frame_id),

crates/debugger_ui/src/session/running/stack_frame_list.rs 🔗

@@ -31,7 +31,7 @@ pub struct StackFrameList {
     invalidate: bool,
     entries: Vec<StackFrameEntry>,
     workspace: WeakEntity<Workspace>,
-    current_stack_frame_id: Option<StackFrameId>,
+    selected_stack_frame_id: Option<StackFrameId>,
     scrollbar_state: ScrollbarState,
 }
 
@@ -85,7 +85,7 @@ impl StackFrameList {
             _subscription,
             invalidate: true,
             entries: Default::default(),
-            current_stack_frame_id: None,
+            selected_stack_frame_id: None,
         }
     }
 
@@ -132,8 +132,8 @@ impl StackFrameList {
             .unwrap_or(0)
     }
 
-    pub fn current_stack_frame_id(&self) -> Option<StackFrameId> {
-        self.current_stack_frame_id
+    pub fn selected_stack_frame_id(&self) -> Option<StackFrameId> {
+        self.selected_stack_frame_id
     }
 
     pub(super) fn refresh(&mut self, cx: &mut Context<Self>) {
@@ -188,20 +188,20 @@ impl StackFrameList {
     }
 
     pub fn go_to_selected_stack_frame(&mut self, window: &Window, cx: &mut Context<Self>) {
-        if let Some(current_stack_frame_id) = self.current_stack_frame_id {
+        if let Some(selected_stack_frame_id) = self.selected_stack_frame_id {
             let frame = self
                 .entries
                 .iter()
                 .find_map(|entry| match entry {
                     StackFrameEntry::Normal(dap) => {
-                        if dap.id == current_stack_frame_id {
+                        if dap.id == selected_stack_frame_id {
                             Some(dap)
                         } else {
                             None
                         }
                     }
                     StackFrameEntry::Collapsed(daps) => {
-                        daps.iter().find(|dap| dap.id == current_stack_frame_id)
+                        daps.iter().find(|dap| dap.id == selected_stack_frame_id)
                     }
                 })
                 .cloned();
@@ -220,7 +220,7 @@ impl StackFrameList {
         window: &Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
-        self.current_stack_frame_id = Some(stack_frame.id);
+        self.selected_stack_frame_id = Some(stack_frame.id);
 
         cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
             stack_frame.id,
@@ -319,7 +319,7 @@ impl StackFrameList {
         cx: &mut Context<Self>,
     ) -> AnyElement {
         let source = stack_frame.source.clone();
-        let is_selected_frame = Some(stack_frame.id) == self.current_stack_frame_id;
+        let is_selected_frame = Some(stack_frame.id) == self.selected_stack_frame_id;
 
         let formatted_path = format!(
             "{}:{}",

crates/debugger_ui/src/tests/stack_frame_list.rs 🔗

@@ -191,7 +191,7 @@ async fn test_fetch_initial_stack_frames_and_go_to_stack_frame(
             .update(cx, |state, _| state.stack_frame_list().clone());
 
         stack_frame_list.update(cx, |stack_frame_list, cx| {
-            assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
+            assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
             assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
         });
     });
@@ -425,7 +425,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
         .unwrap();
 
     stack_frame_list.update(cx, |stack_frame_list, cx| {
-        assert_eq!(Some(1), stack_frame_list.current_stack_frame_id());
+        assert_eq!(Some(1), stack_frame_list.selected_stack_frame_id());
         assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
     });
 
@@ -440,7 +440,7 @@ async fn test_select_stack_frame(executor: BackgroundExecutor, cx: &mut TestAppC
     cx.run_until_parked();
 
     stack_frame_list.update(cx, |stack_frame_list, cx| {
-        assert_eq!(Some(2), stack_frame_list.current_stack_frame_id());
+        assert_eq!(Some(2), stack_frame_list.selected_stack_frame_id());
         assert_eq!(stack_frames, stack_frame_list.dap_stack_frames(cx));
     });
 

crates/debugger_ui/src/tests/variable_list.rs 🔗

@@ -212,7 +212,7 @@ async fn test_basic_fetch_initial_scope_and_variables(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.current_stack_frame_id())
+                (list.flatten_entries(), list.selected_stack_frame_id())
             });
 
         assert_eq!(stack_frames, stack_frame_list);
@@ -483,7 +483,7 @@ async fn test_fetch_variables_for_multiple_scopes(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.current_stack_frame_id())
+                (list.flatten_entries(), list.selected_stack_frame_id())
             });
 
         assert_eq!(Some(1), stack_frame_id);
@@ -1565,7 +1565,7 @@ async fn test_variable_list_only_sends_requests_when_rendering(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.current_stack_frame_id())
+                (list.flatten_entries(), list.selected_stack_frame_id())
             });
 
         assert_eq!(Some(1), stack_frame_id);
@@ -1877,7 +1877,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.current_stack_frame_id())
+                (list.flatten_entries(), list.selected_stack_frame_id())
             });
 
         let variable_list = running_state.variable_list().read(cx);
@@ -1888,7 +1888,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
             running_state
                 .stack_frame_list()
                 .read(cx)
-                .current_stack_frame_id(),
+                .selected_stack_frame_id(),
             Some(1)
         );
 
@@ -1934,7 +1934,7 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame(
     running_state.update(cx, |running_state, cx| {
         let (stack_frame_list, stack_frame_id) =
             running_state.stack_frame_list().update(cx, |list, _| {
-                (list.flatten_entries(), list.current_stack_frame_id())
+                (list.flatten_entries(), list.selected_stack_frame_id())
             });
 
         let variable_list = running_state.variable_list().read(cx);

crates/editor/Cargo.toml 🔗

@@ -35,6 +35,7 @@ assets.workspace = true
 client.workspace = true
 clock.workspace = true
 collections.workspace = true
+command_palette_hooks.workspace = true
 convert_case.workspace = true
 db.workspace = true
 buffer_diff.workspace = true

crates/editor/src/actions.rs 🔗

@@ -408,6 +408,8 @@ actions!(
         DisableBreakpoint,
         EnableBreakpoint,
         EditLogBreakpoint,
+        DebuggerRunToCursor,
+        DebuggerEvaluateSelectedText,
         ToggleAutoSignatureHelp,
         ToggleGitBlameInline,
         OpenGitBlameCommit,

crates/editor/src/editor.rs 🔗

@@ -6415,6 +6415,9 @@ impl Editor {
             "Set Breakpoint"
         };
 
+        let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
+            .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
+
         let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
             BreakpointState::Enabled => Some("Disable"),
             BreakpointState::Disabled => Some("Enable"),
@@ -6426,6 +6429,21 @@ impl Editor {
         ui::ContextMenu::build(window, cx, |menu, _, _cx| {
             menu.on_blur_subscription(Subscription::new(|| {}))
                 .context(focus_handle)
+                .when(run_to_cursor, |this| {
+                    let weak_editor = weak_editor.clone();
+                    this.entry("Run to cursor", None, move |window, cx| {
+                        weak_editor
+                            .update(cx, |editor, cx| {
+                                editor.change_selections(None, window, cx, |s| {
+                                    s.select_ranges([Point::new(row, 0)..Point::new(row, 0)])
+                                });
+                            })
+                            .ok();
+
+                        window.dispatch_action(Box::new(DebuggerRunToCursor), cx);
+                    })
+                    .separator()
+                })
                 .when_some(toggle_state_msg, |this, msg| {
                     this.entry(msg, None, {
                         let weak_editor = weak_editor.clone();

crates/editor/src/mouse_context_menu.rs 🔗

@@ -1,10 +1,10 @@
-use crate::CopyAndTrim;
-use crate::actions::FormatSelections;
 use crate::{
-    Copy, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor, EditorMode,
-    FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation, GoToTypeDefinition,
-    Paste, Rename, RevealInFileManager, SelectMode, ToDisplayPoint, ToggleCodeActions,
-    actions::Format, selections_collection::SelectionsCollection,
+    Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint,
+    DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration, GoToDefinition,
+    GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode,
+    ToDisplayPoint, ToggleCodeActions,
+    actions::{Format, FormatSelections},
+    selections_collection::SelectionsCollection,
 };
 use gpui::prelude::FluentBuilder;
 use gpui::{Context, DismissEvent, Entity, Focusable as _, Pixels, Point, Subscription, Window};
@@ -169,9 +169,19 @@ pub fn deploy_context_menu(
                 .is_some()
         });
 
+        let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx)
+            .map_or(false, |filter| {
+                !filter.is_hidden(&DebuggerEvaluateSelectedText)
+            });
+
         ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
             let builder = menu
                 .on_blur_subscription(Subscription::new(|| {}))
+                .when(evaluate_selection && has_selections, |builder| {
+                    builder
+                        .action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))
+                        .separator()
+                })
                 .action("Go to Definition", Box::new(GoToDefinition))
                 .action("Go to Declaration", Box::new(GoToDeclaration))
                 .action("Go to Type Definition", Box::new(GoToTypeDefinition))

crates/project/src/debugger/breakpoint_store.rs 🔗

@@ -218,7 +218,7 @@ impl BreakpointStore {
         }
     }
 
-    fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
+    pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
         worktree::File::from_dyn(buffer.read(cx).file())
             .and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
             .map(Arc::<Path>::from)

crates/project/src/debugger/session.rs 🔗

@@ -1,6 +1,8 @@
 use crate::project_settings::ProjectSettings;
 
-use super::breakpoint_store::{BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason};
+use super::breakpoint_store::{
+    BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint,
+};
 use super::dap_command::{
     self, Attach, ConfigurationDone, ContinueCommand, DapCommand, DisconnectCommand,
     EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand,
@@ -163,6 +165,7 @@ pub struct LocalMode {
     config: DebugAdapterConfig,
     adapter: Arc<dyn DebugAdapter>,
     breakpoint_store: Entity<BreakpointStore>,
+    tmp_breakpoint: Option<SourceBreakpoint>,
 }
 
 fn client_source(abs_path: &Path) -> dap::Source {
@@ -383,6 +386,7 @@ impl LocalMode {
                 client,
                 adapter,
                 breakpoint_store,
+                tmp_breakpoint: None,
                 config: config.clone(),
             };
 
@@ -431,6 +435,7 @@ impl LocalMode {
             .read_with(cx, |store, cx| store.breakpoints_from_path(&abs_path, cx))
             .into_iter()
             .filter(|bp| bp.state.is_enabled())
+            .chain(self.tmp_breakpoint.clone())
             .map(Into::into)
             .collect();
 
@@ -1040,6 +1045,40 @@ impl Session {
         }
     }
 
+    pub fn run_to_position(
+        &mut self,
+        breakpoint: SourceBreakpoint,
+        active_thread_id: ThreadId,
+        cx: &mut Context<Self>,
+    ) {
+        match &mut self.mode {
+            Mode::Local(local_mode) => {
+                if !matches!(
+                    self.thread_states.thread_state(active_thread_id),
+                    Some(ThreadStatus::Stopped)
+                ) {
+                    return;
+                };
+                let path = breakpoint.path.clone();
+                local_mode.tmp_breakpoint = Some(breakpoint);
+                let task = local_mode.send_breakpoints_from_path(
+                    path,
+                    BreakpointUpdatedReason::Toggled,
+                    cx,
+                );
+
+                cx.spawn(async move |this, cx| {
+                    task.await;
+                    this.update(cx, |this, cx| {
+                        this.continue_thread(active_thread_id, cx);
+                    })
+                })
+                .detach();
+            }
+            Mode::Remote(_) => {}
+        }
+    }
+
     pub fn output(
         &self,
         since: OutputToken,
@@ -1086,6 +1125,16 @@ impl Session {
     }
 
     fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context<Self>) {
+        if let Some((local, path)) = self.as_local_mut().and_then(|local| {
+            let breakpoint = local.tmp_breakpoint.take()?;
+            let path = breakpoint.path.clone();
+            Some((local, path))
+        }) {
+            local
+                .send_breakpoints_from_path(path, BreakpointUpdatedReason::Toggled, cx)
+                .detach();
+        };
+
         if event.all_threads_stopped.unwrap_or_default() || event.thread_id.is_none() {
             self.thread_states.stop_all_threads();