diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 3c5a9e85ef7b3f..1b35d217bf4cf3 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1,13 +1,19 @@ use call::ActiveCall; use dap::{ - requests::{Disconnect, Initialize, Launch, RestartFrame, StackTrace}, - StackFrame, + requests::{Disconnect, Initialize, Launch, RestartFrame, SetBreakpoints, StackTrace}, + SourceBreakpoint, StackFrame, }; use debugger_ui::debugger_panel::DebugPanel; +use editor::Editor; use gpui::{TestAppContext, View, VisualTestContext}; -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, +use project::ProjectPath; +use serde_json::json; +use std::{ + path::Path, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, }; use workspace::{dock::Panel, Workspace}; @@ -827,3 +833,262 @@ async fn test_restart_stack_frame(cx_a: &mut TestAppContext, cx_b: &mut TestAppC shutdown_client.await.unwrap(); } + +#[gpui::test] +async fn test_updated_breakpoints_send_to_dap( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let executor = cx_a.executor(); + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "test.txt": "one\ntwo\nthree\nfour\nfive", + }), + ) + .await; + + init_test(cx_a); + init_test(cx_b); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + let project_path = ProjectPath { + worktree_id, + path: Arc::from(Path::new(&"test.txt")), + }; + + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + let project_b = client_b.join_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + + add_debugger_panel(&workspace_a, cx_a).await; + add_debugger_panel(&workspace_b, cx_b).await; + + let task = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |store, cx| { + store.start_debug_session( + dap::DebugAdapterConfig { + label: "test config".into(), + kind: dap::DebugAdapterKind::Fake, + request: dap::DebugRequestType::Launch, + program: None, + cwd: None, + initialize_args: None, + }, + cx, + ) + }) + }); + + let (session, client) = task.await.unwrap(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_restart_frame: Some(true), + ..Default::default() + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!( + vec![SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + }], + args.breakpoints.unwrap() + ); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + // Client B opens an editor. + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor_b.update(cx_b, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + // Client A opens an editor. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert!(args.breakpoints.unwrap().is_empty()); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + // remove the breakpoint that client B added + editor_a.update(cx_a, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called" + ); + + let called_set_breakpoints = Arc::new(AtomicBool::new(false)); + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!( + vec![ + SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + }, + SourceBreakpoint { + line: 2, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None + } + ], + args.breakpoints.unwrap() + ); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + // Add our own breakpoint now + editor_a.update(cx_a, |editor, cx| { + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + editor.move_up(&editor::actions::MoveUp, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request must be called" + ); + + let shutdown_client = project_a.update(cx_a, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(&session.read(cx).id(), cx) + }) + }); + + shutdown_client.await.unwrap(); +} diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index e39be89576c3f2..e1cdcb968b48c4 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2466,6 +2466,7 @@ async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppConte .unwrap() .downcast::() .unwrap(); + cx_a.run_until_parked(); cx_b.run_until_parked(); diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs index 735adb5bfd3d61..447350bb88477c 100644 --- a/crates/dap/src/session.rs +++ b/crates/dap/src/session.rs @@ -97,4 +97,8 @@ impl DebugSession { pub fn clients(&self) -> impl Iterator> + '_ { self.clients.values().cloned() } + + pub fn client_ids(&self) -> impl Iterator + '_ { + self.clients.keys().cloned() + } } diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 9378a4970ec306..fee94a9326da4e 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -194,7 +194,7 @@ impl DebugPanel { cx.notify(); } project::Event::SetDebugClient(set_debug_client) => { - let _res = this.handle_set_debug_panel_item(set_debug_client, cx); + this.handle_set_debug_panel_item(set_debug_client, cx); } _ => {} } diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 9711f75a957cbb..65539492969f6d 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -75,7 +75,7 @@ pub enum DapStoreEvent { message: Message, }, Notification(String), - BreakpointsChanged, + BreakpointsChanged(ProjectPath), ActiveDebugLineChanged, SetDebugPanelItem(SetDebuggerPanelItem), UpdateDebugAdapter(UpdateDebugAdapter), @@ -278,7 +278,7 @@ impl DapStore { pub fn client_by_id( &self, client_id: &DebugAdapterClientId, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Option<(Model, Arc)> { let session = self.session_by_client_id(client_id)?; let client = session.read(cx).client_by_id(client_id)?; @@ -1687,10 +1687,10 @@ impl DapStore { if breakpoints.is_empty() { store.breakpoints.remove(&project_path); } else { - store.breakpoints.insert(project_path, breakpoints); + store.breakpoints.insert(project_path.clone(), breakpoints); } - cx.emit(DapStoreEvent::BreakpointsChanged); + cx.emit(DapStoreEvent::BreakpointsChanged(project_path)); cx.notify(); }) @@ -1797,11 +1797,9 @@ impl DapStore { &mut self, project_path: &ProjectPath, mut breakpoint: Breakpoint, - buffer_path: PathBuf, - buffer_snapshot: BufferSnapshot, edit_action: BreakpointEditAction, cx: &mut ModelContext, - ) -> Task> { + ) { let upstream_client = self.upstream_client(); let breakpoint_set = self.breakpoints.entry(project_path.clone()).or_default(); @@ -1840,9 +1838,8 @@ impl DapStore { self.breakpoints.remove(project_path); } + cx.emit(DapStoreEvent::BreakpointsChanged(project_path.clone())); cx.notify(); - - self.send_changed_breakpoints(project_path, buffer_path, buffer_snapshot, cx) } pub fn send_breakpoints( @@ -1851,7 +1848,7 @@ impl DapStore { absolute_file_path: Arc, mut breakpoints: Vec, ignore: bool, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let Some((_, client)) = self.client_by_id(client_id, cx) else { return Task::ready(Err(anyhow!("Could not find client: {:?}", client_id))); @@ -1889,9 +1886,9 @@ impl DapStore { pub fn send_changed_breakpoints( &self, project_path: &ProjectPath, - buffer_path: PathBuf, + absolute_path: PathBuf, buffer_snapshot: BufferSnapshot, - cx: &mut ModelContext, + cx: &ModelContext, ) -> Task> { let Some(local_store) = self.as_local() else { return Task::ready(Err(anyhow!("cannot start session on remote side"))); @@ -1913,7 +1910,7 @@ impl DapStore { for client in session.clients().collect::>() { tasks.push(self.send_breakpoints( &client.id(), - Arc::from(buffer_path.clone()), + Arc::from(absolute_path.clone()), source_breakpoints.clone(), ignore_breakpoints, cx, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 187df244d9d196..a27b8dfc5fc584 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1434,29 +1434,11 @@ impl Project { return; }; - let Some((project_path, buffer_path)) = maybe!({ - let project_path = buffer.read(cx).project_path(cx)?; - let worktree = self.worktree_for_id(project_path.clone().worktree_id, cx)?; - Some(( - project_path.clone(), - worktree.read(cx).absolutize(&project_path.path).ok()?, - )) - }) else { - return; - }; - - self.dap_store.update(cx, |store, cx| { - store - .toggle_breakpoint_for_buffer( - &project_path, - breakpoint, - buffer_path, - buffer.read(cx).snapshot(), - edit_action, - cx, - ) - .detach_and_log_err(cx); - }); + if let Some(project_path) = buffer.read(cx).project_path(cx) { + self.dap_store.update(cx, |store, cx| { + store.toggle_breakpoint_for_buffer(&project_path, breakpoint, edit_action, cx) + }); + } } #[cfg(any(test, feature = "test-support"))] @@ -2536,8 +2518,36 @@ impl Project { message: message.clone(), }); } - DapStoreEvent::BreakpointsChanged => { - cx.notify(); + DapStoreEvent::BreakpointsChanged(project_path) => { + cx.notify(); // so the UI updates + + let buffer_id = self + .buffer_store + .read(cx) + .buffer_id_for_project_path(&project_path); + + let Some(buffer_id) = buffer_id else { + return; + }; + + let Some(buffer) = self.buffer_for_id(*buffer_id, cx) else { + return; + }; + + let Some(absolute_path) = self.absolute_path(project_path, cx) else { + return; + }; + + self.dap_store.update(cx, |store, cx| { + store + .send_changed_breakpoints( + project_path, + absolute_path, + buffer.read(cx).snapshot(), + cx, + ) + .detach_and_log_err(cx); + }); } DapStoreEvent::ActiveDebugLineChanged => { cx.emit(Event::ActiveDebugLineChanged);