diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 7725409c3b..9deab383cc 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -24,8 +24,8 @@ use ui::prelude::*; use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - pane, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut, StepOver, Stop, - ToggleIgnoreBreakpoints, Workspace, + pane, ClearBreakpoints, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, + StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, Workspace, }; pub enum DebugPanelEvent { @@ -174,6 +174,15 @@ impl DebugPanel { workspace.update_in(cx, |workspace, window, cx| { let debug_panel = DebugPanel::new(workspace, window, cx); + workspace.register_action(|workspace, _: &ClearBreakpoints, _, cx| { + workspace.project().read(cx).breakpoint_store().update( + cx, + |breakpoint_store, cx| { + breakpoint_store.clear_breakpoints(cx); + }, + ) + }); + cx.observe(&debug_panel, |_, debug_panel, cx| { let (has_active_session, supports_restart, support_step_back) = debug_panel .update(cx, |this, cx| { diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 3d14c41936..e9a357a083 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -1264,6 +1264,139 @@ async fn test_send_breakpoints_when_editor_has_been_saved( shutdown_session.await.unwrap(); } +#[gpui::test] +async fn test_unsetting_breakpoints_on_clear_breakpoint_action( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + fs.insert_tree( + path!("/project"), + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + "second.rs": "First line\nSecond line\nThird line\nFourth line", + "no_breakpoints.rs": "Used to ensure that we don't unset breakpoint in files with no breakpoints" + }), + ) + .await; + + let project = Project::test(fs, [path!("/project").as_ref()], cx).await; + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let project_path = Path::new(path!("/project")); + let worktree = project + .update(cx, |project, cx| project.find_worktree(project_path, cx)) + .expect("This worktree should exist in project") + .0; + + let worktree_id = workspace + .update(cx, |_, _, cx| worktree.read(cx).id()) + .unwrap(); + + let first = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + + let second = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "second.rs"), cx) + }) + .await + .unwrap(); + + let (first_editor, cx) = cx.add_window_view(|window, cx| { + Editor::new( + EditorMode::Full, + MultiBuffer::build_from_buffer(first, cx), + Some(project.clone()), + window, + cx, + ) + }); + + let (second_editor, cx) = cx.add_window_view(|window, cx| { + Editor::new( + EditorMode::Full, + MultiBuffer::build_from_buffer(second, cx), + Some(project.clone()), + window, + cx, + ) + }); + + first_editor.update_in(cx, |editor, window, cx| { + editor.move_down(&actions::MoveDown, window, cx); + editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); + editor.move_down(&actions::MoveDown, window, cx); + editor.move_down(&actions::MoveDown, window, cx); + editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); + }); + + second_editor.update_in(cx, |editor, window, cx| { + editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); + editor.move_down(&actions::MoveDown, window, cx); + editor.move_down(&actions::MoveDown, window, cx); + editor.move_down(&actions::MoveDown, window, cx); + editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx); + }); + + let task = project.update(cx, |project, cx| { + project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) + }); + + let session = task.await.unwrap(); + let client = session.update(cx, |session, _| session.adapter_client().unwrap()); + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert!( + args.breakpoints.is_none_or(|bps| bps.is_empty()), + "Send empty breakpoint sets to clear them from DAP servers" + ); + + match args + .source + .path + .expect("We should always send a breakpoint's path") + .as_str() + { + "/project/main.rs" | "/project/second.rs" => {} + _ => { + panic!("Unset breakpoints for path that doesn't have any") + } + } + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + cx.dispatch_action(workspace::ClearBreakpoints); + cx.run_until_parked(); + + let shutdown_session = project.update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(session.read(cx).session_id(), cx) + }) + }); + + shutdown_session.await.unwrap(); +} + #[gpui::test] async fn test_debug_session_is_shutdown_when_attach_and_launch_request_fails( executor: BackgroundExecutor, diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index 0a088c8808..365c52525a 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/crates/project/src/debugger/breakpoint_store.rs @@ -5,7 +5,7 @@ use anyhow::{anyhow, Result}; use breakpoints_in_file::BreakpointsInFile; use collections::BTreeMap; use dap::client::SessionId; -use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Task}; +use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task}; use language::{proto::serialize_anchor as serialize_text_anchor, Buffer, BufferSnapshot}; use rpc::{ proto::{self}, @@ -31,7 +31,7 @@ mod breakpoints_in_file { pub(super) buffer: Entity, // TODO: This is.. less than ideal, as it's O(n) and does not return entries in order. We'll have to change TreeMap to support passing in the context for comparisons pub(super) breakpoints: Vec<(text::Anchor, Breakpoint)>, - _subscription: Arc, + _subscription: Arc, } impl BreakpointsInFile { @@ -341,6 +341,12 @@ impl BreakpointStore { } } + pub fn clear_breakpoints(&mut self, cx: &mut Context) { + let breakpoint_paths = self.breakpoints.keys().cloned().collect(); + self.breakpoints.clear(); + cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths)); + } + pub fn breakpoints<'a>( &'a self, buffer: &'a Entity, @@ -498,6 +504,11 @@ impl BreakpointStore { Task::ready(Ok(())) } } + + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn breakpoint_paths(&self) -> Vec> { + self.breakpoints.keys().cloned().collect() + } } #[derive(Clone, Copy)] @@ -509,6 +520,7 @@ pub enum BreakpointUpdatedReason { pub enum BreakpointStoreEvent { ActiveDebugLineChanged, BreakpointsUpdated(Arc, BreakpointUpdatedReason), + BreakpointsCleared(Vec>), } impl EventEmitter for BreakpointStore {} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 978bf165fb..5579bb51a0 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -237,6 +237,19 @@ impl LocalMode { .on_request::(move |_, _| Ok(caps.clone())) .await; + let paths = cx.update(|cx| session.breakpoint_store.read(cx).breakpoint_paths()).expect("Breakpoint store should exist in all tests that start debuggers"); + + session.client.on_request::(move |_, args| { + let p = Arc::from(Path::new(&args.source.path.unwrap())); + if !paths.contains(&p) { + panic!("Sent breakpoints for path without any") + } + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + }).await; + match config.request.clone() { dap::DebugRequestType::Launch if fail => { session @@ -307,6 +320,34 @@ impl LocalMode { }) } + fn unset_breakpoints_from_paths(&self, paths: &Vec>, cx: &mut App) -> Task<()> { + let tasks: Vec<_> = paths + .into_iter() + .map(|path| { + self.request( + dap_command::SetBreakpoints { + source: client_source(path), + source_modified: None, + breakpoints: vec![], + }, + cx.background_executor().clone(), + ) + }) + .collect(); + + cx.background_spawn(async move { + futures::future::join_all(tasks) + .await + .iter() + .for_each(|res| match res { + Ok(_) => {} + Err(err) => { + log::warn!("Set breakpoints request failed: {}", err); + } + }); + }) + } + fn send_breakpoints_from_path( &self, abs_path: Arc, @@ -752,6 +793,14 @@ impl Session { .detach(); }; } + BreakpointStoreEvent::BreakpointsCleared(paths) => { + if let Some(local) = (!this.ignore_breakpoints) + .then(|| this.as_local_mut()) + .flatten() + { + local.unset_breakpoints_from_paths(paths, cx).detach(); + } + } BreakpointStoreEvent::ActiveDebugLineChanged => {} }) .detach(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f38bca1c47..c3c4db40e4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -140,7 +140,8 @@ actions!( StepOut, StepBack, Stop, - ToggleIgnoreBreakpoints + ToggleIgnoreBreakpoints, + ClearBreakpoints ] );