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

## 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>
This commit is contained in:
Anthony Eid 2025-04-09 15:57:29 -04:00 committed by GitHub
parent 780143298a
commit 2752c08810
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 334 additions and 102 deletions

1
Cargo.lock generated
View file

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

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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),

View file

@ -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!(
"{}:{}",

View file

@ -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));
});

View file

@ -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);

View file

@ -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

View file

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

View file

@ -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();

View file

@ -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))

View file

@ -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)

View file

@ -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();