diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 5a065878af4ab7..c36207514aed29 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -474,6 +474,7 @@ CREATE TABLE IF NOT EXISTS "debug_clients" ( project_id INTEGER NOT NULL, session_id BIGINT NOT NULL, capabilities INTEGER NOT NULL, + ignore_breakpoints BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY (id, project_id, session_id), FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE ); diff --git a/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql b/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql new file mode 100644 index 00000000000000..e1e362c5cf84ad --- /dev/null +++ b/crates/collab/migrations/20250121181012_add_ignore_breakpoints.sql @@ -0,0 +1,2 @@ + +ALTER TABLE debug_clients ADD COLUMN ignore_breakpoints BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index da0d99432be96a..b25c754d83c7a6 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -556,6 +556,40 @@ impl Database { .await } + pub async fn ignore_breakpoint_state( + &self, + connection_id: ConnectionId, + update: &proto::IgnoreBreakpointState, + ) -> Result>> { + let project_id = ProjectId::from_proto(update.project_id); + self.project_transaction(project_id, |tx| async move { + let debug_clients = debug_clients::Entity::find() + .filter( + Condition::all() + .add(debug_clients::Column::ProjectId.eq(project_id)) + .add(debug_clients::Column::SessionId.eq(update.session_id)), + ) + .all(&*tx) + .await?; + + for debug_client in debug_clients { + debug_clients::Entity::update(debug_clients::ActiveModel { + id: ActiveValue::Unchanged(debug_client.id), + project_id: ActiveValue::Unchanged(debug_client.project_id), + session_id: ActiveValue::Unchanged(debug_client.session_id), + capabilities: ActiveValue::Unchanged(debug_client.capabilities), + ignore_breakpoints: ActiveValue::Set(update.ignore), + }) + .exec(&*tx) + .await?; + } + + self.internal_project_connection_ids(project_id, connection_id, true, &tx) + .await + }) + .await + } + pub async fn update_debug_adapter( &self, connection_id: ConnectionId, @@ -647,6 +681,7 @@ impl Database { project_id: ActiveValue::Set(project_id), session_id: ActiveValue::Set(update.session_id as i64), capabilities: ActiveValue::Set(0), + ignore_breakpoints: ActiveValue::Set(false), }; new_debug_client.insert(&*tx).await?; } @@ -729,6 +764,7 @@ impl Database { project_id: ActiveValue::Set(project_id), session_id: ActiveValue::Set(update.session_id as i64), capabilities: ActiveValue::Set(0), + ignore_breakpoints: ActiveValue::Set(false), }; debug_client = Some(new_debug_client.insert(&*tx).await?); } @@ -742,6 +778,7 @@ impl Database { project_id: ActiveValue::Unchanged(debug_client.project_id), session_id: ActiveValue::Unchanged(debug_client.session_id), capabilities: ActiveValue::Set(debug_client.capabilities), + ignore_breakpoints: ActiveValue::Set(debug_client.ignore_breakpoints), }) .exec(&*tx) .await?; @@ -1086,6 +1123,7 @@ impl Database { for (session_id, clients) in debug_sessions.into_iter() { let mut debug_clients = Vec::default(); + let ignore_breakpoints = clients.iter().any(|debug| debug.ignore_breakpoints); // Temp solution until client -> session change for debug_client in clients.into_iter() { let debug_panel_items = debug_client @@ -1108,6 +1146,7 @@ impl Database { project_id, session_id: session_id as u64, clients: debug_clients, + ignore_breakpoints, }); } diff --git a/crates/collab/src/db/tables/debug_clients.rs b/crates/collab/src/db/tables/debug_clients.rs index 02758acaa0c4fa..498b9ed7359091 100644 --- a/crates/collab/src/db/tables/debug_clients.rs +++ b/crates/collab/src/db/tables/debug_clients.rs @@ -25,6 +25,7 @@ pub struct Model { pub session_id: i64, #[sea_orm(column_type = "Integer")] pub capabilities: i32, + pub ignore_breakpoints: bool, } impl Model { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index cf7083a21c1a99..fa116c8cc54938 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -438,6 +438,13 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_message_handler( broadcast_project_message_from_host::, + ) + .add_message_handler( + broadcast_project_message_from_host::, + ) + .add_message_handler(ignore_breakpoint_state) + .add_message_handler( + broadcast_project_message_from_host::, ); Arc::new(server) @@ -2155,6 +2162,28 @@ async fn shutdown_debug_client( Ok(()) } +async fn ignore_breakpoint_state( + request: proto::IgnoreBreakpointState, + session: Session, +) -> Result<()> { + let guest_connection_ids = session + .db() + .await + .ignore_breakpoint_state(session.connection_id, &request) + .await?; + + broadcast( + Some(session.connection_id), + guest_connection_ids.iter().copied(), + |connection_id| { + session + .peer + .forward_send(session.connection_id, connection_id, request.clone()) + }, + ); + Ok(()) +} + /// Notify other participants that a debug panel item has been updated async fn update_debug_adapter(request: proto::UpdateDebugAdapter, session: Session) -> Result<()> { let guest_connection_ids = session diff --git a/crates/collab/src/tests/debug_panel_tests.rs b/crates/collab/src/tests/debug_panel_tests.rs index 045cb6db281ebf..abe5daae104eae 100644 --- a/crates/collab/src/tests/debug_panel_tests.rs +++ b/crates/collab/src/tests/debug_panel_tests.rs @@ -1855,3 +1855,495 @@ async fn test_variable_list( shutdown_client.await.unwrap(); } + +#[gpui::test] +async fn test_ignore_breakpoints( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &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; + let client_c = server.create_client(cx_c, "user_c").await; + + client_a + .fs() + .insert_tree( + "/a", + json!({ + "test.txt": "one\ntwo\nthree\nfour\nfive", + }), + ) + .await; + + init_test(cx_a); + init_test(cx_b); + init_test(cx_c); + + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.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 local_editor = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + local_editor.update(cx_a, |editor, cx| { + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 2 + editor.move_down(&editor::actions::MoveDown, cx); + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 3 + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + 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(); + let client_id = client.id(); + + client + .on_request::(move |_, _| { + Ok(dap::Capabilities { + supports_configuration_done_request: Some(true), + ..Default::default() + }) + }) + .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()); + + let mut actual_breakpoints = args.breakpoints.unwrap(); + actual_breakpoints.sort_by_key(|b| b.line); + + let expected_breakpoints = vec![ + SourceBreakpoint { + line: 2, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None, + }, + SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None, + }, + ]; + + assert_eq!(actual_breakpoints, expected_breakpoints); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + client + .on_request::(move |_, _| { + Ok(dap::StackTraceResponse { + stack_frames: Vec::default(), + total_frames: None, + }) + }) + .await; + + client.on_request::(move |_, _| Ok(())).await; + + client + .fake_event(dap::messages::Events::Initialized(Some( + dap::Capabilities { + supports_configuration_done_request: Some(true), + ..Default::default() + }, + ))) + .await; + + 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 when starting debug session" + ); + + 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(); + + let remote_debug_item = workspace_b.update(cx_b, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + + let session_id = debug_panel.update(cx, |this, cx| { + this.dap_store() + .read(cx) + .as_remote() + .unwrap() + .session_by_client_id(&client.id()) + .unwrap() + .read(cx) + .id() + }); + + let breakpoints_ignored = active_debug_panel_item.read(cx).are_breakpoints_ignored(cx); + + assert_eq!(session_id, active_debug_panel_item.read(cx).session_id()); + assert_eq!(false, breakpoints_ignored); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + called_set_breakpoints.store(false, Ordering::SeqCst); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!(args.breakpoints, Some(vec![])); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + let local_debug_item = workspace_a.update(cx_a, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + + assert_eq!( + false, + active_debug_panel_item.read(cx).are_breakpoints_ignored(cx) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + + active_debug_panel_item + }); + + local_debug_item.update(cx_a, |item, cx| { + item.toggle_ignore_breakpoints(cx); // Set to true + assert_eq!(true, item.are_breakpoints_ignored(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 to ignore breakpoints" + ); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, _args| { + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + let remote_editor = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path(project_path.clone(), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + called_set_breakpoints.store(false, std::sync::atomic::Ordering::SeqCst); + + remote_editor.update(cx_b, |editor, cx| { + editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, cx); // Line 1 + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request be called whenever breakpoints are toggled but with not breakpoints" + ); + + remote_debug_item.update(cx_b, |debug_panel, cx| { + let breakpoints_ignored = debug_panel.are_breakpoints_ignored(cx); + + assert_eq!(true, breakpoints_ignored); + assert_eq!(client.id(), debug_panel.client_id()); + assert_eq!(1, debug_panel.thread_id()); + }); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + + let mut actual_breakpoints = args.breakpoints.unwrap(); + actual_breakpoints.sort_by_key(|b| b.line); + + let expected_breakpoints = vec![ + SourceBreakpoint { + line: 1, + 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, + }, + SourceBreakpoint { + line: 3, + column: None, + condition: None, + hit_condition: None, + log_message: None, + mode: None, + }, + ]; + + assert_eq!(actual_breakpoints, expected_breakpoints); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + let project_c = client_c.join_remote_project(project_id, cx_c).await; + active_call_c + .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) + .await + .unwrap(); + + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + add_debugger_panel(&workspace_c, cx_c).await; + + let last_join_remote_item = workspace_c.update(cx_c, |workspace, cx| { + let debug_panel = workspace.panel::(cx).unwrap(); + let active_debug_panel_item = debug_panel + .update(cx, |this, cx| this.active_debug_panel_item(cx)) + .unwrap(); + + let breakpoints_ignored = active_debug_panel_item.read(cx).are_breakpoints_ignored(cx); + + assert_eq!(true, breakpoints_ignored); + + assert_eq!( + 1, + debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len()) + ); + assert_eq!(client.id(), active_debug_panel_item.read(cx).client_id()); + assert_eq!(1, active_debug_panel_item.read(cx).thread_id()); + active_debug_panel_item + }); + + remote_debug_item.update(cx_b, |item, cx| { + item.toggle_ignore_breakpoints(cx); + }); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + cx_c.run_until_parked(); + + assert!( + called_set_breakpoints.load(std::sync::atomic::Ordering::SeqCst), + "SetBreakpoint request should be called to update breakpoints" + ); + + client + .on_request::({ + let called_set_breakpoints = called_set_breakpoints.clone(); + move |_, args| { + assert_eq!("/a/test.txt", args.source.path.unwrap()); + assert_eq!(args.breakpoints, Some(vec![])); + + called_set_breakpoints.store(true, Ordering::SeqCst); + + Ok(dap::SetBreakpointsResponse { + breakpoints: Vec::default(), + }) + } + }) + .await; + + local_debug_item.update(cx_a, |debug_panel_item, cx| { + assert_eq!( + false, + debug_panel_item.are_breakpoints_ignored(cx), + "Remote client set this to false" + ); + }); + + remote_debug_item.update(cx_b, |debug_panel_item, cx| { + assert_eq!( + false, + debug_panel_item.are_breakpoints_ignored(cx), + "Remote client set this to false" + ); + }); + + last_join_remote_item.update(cx_c, |debug_panel_item, cx| { + assert_eq!( + false, + debug_panel_item.are_breakpoints_ignored(cx), + "Remote client set this to false" + ); + }); + + 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(); + + cx_a.run_until_parked(); + cx_b.run_until_parked(); + + project_b.update(cx_b, |project, cx| { + project.dap_store().update(cx, |dap_store, _cx| { + let sessions = dap_store.sessions().collect::>(); + + assert_eq!( + None, + dap_store.session_by_client_id(&client_id), + "No client_id to session mapping should exist after shutdown" + ); + assert_eq!( + 0, + sessions.len(), + "No sessions should be left after shutdown" + ); + }) + }); + + project_c.update(cx_c, |project, cx| { + project.dap_store().update(cx, |dap_store, _cx| { + let sessions = dap_store.sessions().collect::>(); + + assert_eq!( + None, + dap_store.session_by_client_id(&client_id), + "No client_id to session mapping should exist after shutdown" + ); + assert_eq!( + 0, + sessions.len(), + "No sessions should be left after shutdown" + ); + }) + }); +} diff --git a/crates/dap/src/session.rs b/crates/dap/src/session.rs index 77e07a76a526e0..745b0ec5244f53 100644 --- a/crates/dap/src/session.rs +++ b/crates/dap/src/session.rs @@ -19,54 +19,37 @@ impl DebugSessionId { } } -pub struct DebugSession { +pub enum DebugSession { + Local(LocalDebugSession), + Remote(RemoteDebugSession), +} + +pub struct LocalDebugSession { id: DebugSessionId, ignore_breakpoints: bool, configuration: DebugAdapterConfig, clients: HashMap>, } -impl DebugSession { - pub fn new(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { - Self { - id, - configuration, - ignore_breakpoints: false, - clients: HashMap::default(), - } - } - - pub fn id(&self) -> DebugSessionId { - self.id - } - - pub fn name(&self) -> String { - self.configuration.label.clone() - } - +impl LocalDebugSession { pub fn configuration(&self) -> &DebugAdapterConfig { &self.configuration } - pub fn ignore_breakpoints(&self) -> bool { - self.ignore_breakpoints - } - - pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut ModelContext) { - self.ignore_breakpoints = ignore; - cx.notify(); - } - pub fn update_configuration( &mut self, f: impl FnOnce(&mut DebugAdapterConfig), - cx: &mut ModelContext, + cx: &mut ModelContext, ) { f(&mut self.configuration); cx.notify(); } - pub fn add_client(&mut self, client: Arc, cx: &mut ModelContext) { + pub fn add_client( + &mut self, + client: Arc, + cx: &mut ModelContext, + ) { self.clients.insert(client.id(), client); cx.notify(); } @@ -74,7 +57,7 @@ impl DebugSession { pub fn remove_client( &mut self, client_id: &DebugAdapterClientId, - cx: &mut ModelContext, + cx: &mut ModelContext, ) -> Option> { let client = self.clients.remove(client_id); cx.notify(); @@ -101,4 +84,76 @@ impl DebugSession { pub fn client_ids(&self) -> impl Iterator + '_ { self.clients.keys().cloned() } + + pub fn id(&self) -> DebugSessionId { + self.id + } +} + +pub struct RemoteDebugSession { + id: DebugSessionId, + ignore_breakpoints: bool, + label: String, +} + +impl DebugSession { + pub fn new_local(id: DebugSessionId, configuration: DebugAdapterConfig) -> Self { + Self::Local(LocalDebugSession { + id, + ignore_breakpoints: false, + configuration, + clients: HashMap::default(), + }) + } + + pub fn as_local(&self) -> Option<&LocalDebugSession> { + match self { + DebugSession::Local(local) => Some(local), + _ => None, + } + } + + pub fn as_local_mut(&mut self) -> Option<&mut LocalDebugSession> { + match self { + DebugSession::Local(local) => Some(local), + _ => None, + } + } + + pub fn new_remote(id: DebugSessionId, label: String, ignore_breakpoints: bool) -> Self { + Self::Remote(RemoteDebugSession { + id, + label: label.clone(), + ignore_breakpoints, + }) + } + + pub fn id(&self) -> DebugSessionId { + match self { + DebugSession::Local(local) => local.id, + DebugSession::Remote(remote) => remote.id, + } + } + + pub fn name(&self) -> String { + match self { + DebugSession::Local(local) => local.configuration.label.clone(), + DebugSession::Remote(remote) => remote.label.clone(), + } + } + + pub fn ignore_breakpoints(&self) -> bool { + match self { + DebugSession::Local(local) => local.ignore_breakpoints, + DebugSession::Remote(remote) => remote.ignore_breakpoints, + } + } + + pub fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut ModelContext) { + match self { + DebugSession::Local(local) => local.ignore_breakpoints = ignore, + DebugSession::Remote(remote) => remote.ignore_breakpoints = ignore, + } + cx.notify(); + } } diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index c315a12384833c..b111ecb1f8bb93 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -580,25 +580,28 @@ impl DapLogView { .dap_store() .read(cx) .sessions() - .map(|session| DapMenuItem { - session_id: session.read(cx).id(), - session_name: session.read(cx).name(), - clients: { - let mut clients = session - .read(cx) - .clients() - .map(|client| DapMenuSubItem { - client_id: client.id(), - client_name: client.adapter_id(), - has_adapter_logs: client.has_adapter_logs(), - selected_entry: self - .current_view - .map_or(LogKind::Adapter, |(_, kind)| kind), - }) - .collect::>(); - clients.sort_by_key(|item| item.client_id.0); - clients - }, + .filter_map(|session| { + Some(DapMenuItem { + session_id: session.read(cx).id(), + session_name: session.read(cx).name(), + clients: { + let mut clients = session + .read(cx) + .as_local()? + .clients() + .map(|client| DapMenuSubItem { + client_id: client.id(), + client_name: client.adapter_id(), + has_adapter_logs: client.has_adapter_logs(), + selected_entry: self + .current_view + .map_or(LogKind::Adapter, |(_, kind)| kind), + }) + .collect::>(); + clients.sort_by_key(|item| item.client_id.0); + clients + }, + }) }) .collect::>(); menu_items.sort_by_key(|item| item.session_id.0); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index ec343930ef45e2..9d588cd06e8138 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -292,6 +292,11 @@ impl DebugPanel { &self.message_queue } + #[cfg(any(test, feature = "test-support"))] + pub fn dap_store(&self) -> Model { + self.dap_store.clone() + } + pub fn active_debug_panel_item( &self, cx: &mut ViewContext, @@ -565,14 +570,19 @@ impl DebugPanel { client_id: &DebugAdapterClientId, cx: &mut ViewContext, ) { - let Some(session) = self.dap_store.read(cx).session_by_id(session_id) else { + let Some(session) = self + .dap_store + .read(cx) + .session_by_id(session_id) + .and_then(|session| session.read(cx).as_local()) + else { return; }; let session_id = *session_id; let client_id = *client_id; let workspace = self.workspace.clone(); - let request_type = session.read(cx).configuration().request.clone(); + let request_type = session.configuration().request.clone(); cx.spawn(|this, mut cx| async move { let task = this.update(&mut cx, |this, cx| { this.dap_store.update(cx, |store, cx| { @@ -1030,6 +1040,11 @@ impl DebugPanel { ) }); + self.dap_store.update(cx, |dap_store, cx| { + dap_store.add_remote_session(session_id, None, cx); + dap_store.add_client_to_session(session_id, client_id); + }); + pane.add_item(Box::new(debug_panel_item.clone()), true, true, None, cx); debug_panel_item }); diff --git a/crates/debugger_ui/src/debugger_panel_item.rs b/crates/debugger_ui/src/debugger_panel_item.rs index 35ee5957f41737..a645578f3fda1f 100644 --- a/crates/debugger_ui/src/debugger_panel_item.rs +++ b/crates/debugger_ui/src/debugger_panel_item.rs @@ -506,6 +506,12 @@ impl DebugPanelItem { &self.thread_state } + #[cfg(any(test, feature = "test-support"))] + pub fn are_breakpoints_ignored(&self, cx: &AppContext) -> bool { + self.dap_store + .read_with(cx, |dap, cx| dap.ignore_breakpoints(&self.session_id, cx)) + } + pub fn capabilities(&self, cx: &mut ViewContext) -> Capabilities { self.dap_store.read(cx).capabilities_by_id(&self.client_id) } diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index d5f64d97a54176..a779992bd8094c 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -719,7 +719,7 @@ async fn test_handle_start_debugging_reverse_request( cx.run_until_parked(); project.update(cx, |_, cx| { - assert_eq!(2, session.read(cx).clients_len()); + assert_eq!(2, session.read(cx).as_local().unwrap().clients_len()); }); assert!( send_response.load(std::sync::atomic::Ordering::SeqCst), @@ -729,6 +729,8 @@ async fn test_handle_start_debugging_reverse_request( let second_client = project.update(cx, |_, cx| { session .read(cx) + .as_local() + .unwrap() .client_by_id(&DebugAdapterClientId(1)) .unwrap() }); diff --git a/crates/project/src/dap_store.rs b/crates/project/src/dap_store.rs index 3b4a91c02cf81b..aee5ba5da5d4ee 100644 --- a/crates/project/src/dap_store.rs +++ b/crates/project/src/dap_store.rs @@ -36,7 +36,9 @@ use dap_adapters::build_adapter; use fs::Fs; use futures::future::Shared; use futures::FutureExt; -use gpui::{AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task}; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SharedString, Task, +}; use http_client::HttpClient; use language::{ proto::{deserialize_anchor, serialize_anchor as serialize_text_anchor}, @@ -124,9 +126,22 @@ impl LocalDapStore { pub struct RemoteDapStore { upstream_client: Option, upstream_project_id: u64, + sessions: HashMap>, + client_by_session: HashMap, event_queue: Option>, } +impl RemoteDapStore { + pub fn session_by_client_id( + &self, + client_id: &DebugAdapterClientId, + ) -> Option> { + self.client_by_session + .get(client_id) + .and_then(|session_id| self.sessions.get(session_id).cloned()) + } +} + pub struct DapStore { mode: DapStoreMode, downstream_client: Option<(AnyProtoClient, u64)>, @@ -149,6 +164,8 @@ impl DapStore { client.add_model_message_handler(DapStore::handle_synchronize_breakpoints); client.add_model_message_handler(DapStore::handle_update_debug_adapter); client.add_model_message_handler(DapStore::handle_update_thread_status); + client.add_model_message_handler(DapStore::handle_ignore_breakpoint_state); + client.add_model_message_handler(DapStore::handle_session_has_shutdown); client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); @@ -162,7 +179,7 @@ impl DapStore { client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); client.add_model_request_handler(DapStore::handle_dap_command::); - client.add_model_request_handler(DapStore::handle_shutdown_session); + client.add_model_request_handler(DapStore::handle_shutdown_session_request); } pub fn new_local( @@ -204,6 +221,8 @@ impl DapStore { mode: DapStoreMode::Remote(RemoteDapStore { upstream_client: Some(upstream_client), upstream_project_id: project_id, + sessions: Default::default(), + client_by_session: Default::default(), event_queue: Some(VecDeque::default()), }), downstream_client: None, @@ -262,21 +281,91 @@ impl DapStore { self.downstream_client.as_ref() } + pub fn add_remote_session( + &mut self, + session_id: DebugSessionId, + ignore: Option, + cx: &mut ModelContext, + ) { + match &mut self.mode { + DapStoreMode::Remote(remote) => { + remote + .sessions + .entry(session_id) + .or_insert(cx.new_model(|_| { + DebugSession::new_remote( + session_id, + "Remote-Debug".to_owned(), + ignore.unwrap_or(false), + ) + })); + } + _ => {} + } + } + + pub fn add_client_to_session( + &mut self, + session_id: DebugSessionId, + client_id: DebugAdapterClientId, + ) { + match &mut self.mode { + DapStoreMode::Local(local) => { + if local.sessions.contains_key(&session_id) { + local.client_by_session.insert(client_id, session_id); + } + } + DapStoreMode::Remote(remote) => { + if remote.sessions.contains_key(&session_id) { + remote.client_by_session.insert(client_id, session_id); + } + } + } + } + + pub fn remove_session(&mut self, session_id: DebugSessionId, cx: &mut ModelContext) { + match &mut self.mode { + DapStoreMode::Local(local) => { + if let Some(session) = local.sessions.remove(&session_id) { + for client_id in session + .read(cx) + .as_local() + .map(|local| local.client_ids()) + .expect("Local Dap can only have local sessions") + { + local.client_by_session.remove(&client_id); + } + } + } + DapStoreMode::Remote(remote) => { + remote.sessions.remove(&session_id); + remote.client_by_session.retain(|_, val| val != &session_id) + } + } + } + pub fn sessions(&self) -> impl Iterator> + '_ { - self.as_local().unwrap().sessions.values().cloned() + match &self.mode { + DapStoreMode::Local(local) => local.sessions.values().cloned(), + DapStoreMode::Remote(remote) => remote.sessions.values().cloned(), + } } pub fn session_by_id(&self, session_id: &DebugSessionId) -> Option> { - self.as_local() - .and_then(|store| store.sessions.get(session_id).cloned()) + match &self.mode { + DapStoreMode::Local(local) => local.sessions.get(session_id).cloned(), + DapStoreMode::Remote(remote) => remote.sessions.get(session_id).cloned(), + } } pub fn session_by_client_id( &self, client_id: &DebugAdapterClientId, ) -> Option> { - self.as_local() - .and_then(|store| store.session_by_client_id(client_id)) + match &self.mode { + DapStoreMode::Local(local) => local.session_by_client_id(client_id), + DapStoreMode::Remote(remote) => remote.session_by_client_id(client_id), + } } pub fn client_by_id( @@ -284,10 +373,10 @@ impl DapStore { client_id: &DebugAdapterClientId, cx: &ModelContext, ) -> Option<(Model, Arc)> { - let session = self.session_by_client_id(client_id)?; - let client = session.read(cx).client_by_id(client_id)?; + let local_session = self.session_by_client_id(client_id)?; + let client = local_session.read(cx).as_local()?.client_by_id(client_id)?; - Some((session, client)) + Some((local_session, client)) } pub fn capabilities_by_id(&self, client_id: &DebugAdapterClientId) -> Capabilities { @@ -370,7 +459,50 @@ impl DapStore { &self.breakpoints } - pub fn ignore_breakpoints(&self, session_id: &DebugSessionId, cx: &ModelContext) -> bool { + async fn handle_session_has_shutdown( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.remove_session(DebugSessionId::from_proto(envelope.payload.session_id), cx); + })?; + + Ok(()) + } + + async fn handle_ignore_breakpoint_state( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + let session_id = DebugSessionId::from_proto(envelope.payload.session_id); + + this.update(&mut cx, |this, cx| { + if let Some(session) = this.session_by_id(&session_id) { + session.update(cx, |session, cx| { + session.set_ignore_breakpoints(envelope.payload.ignore, cx) + }); + } + })?; + + Ok(()) + } + + pub fn set_ignore_breakpoints( + &mut self, + session_id: &DebugSessionId, + ignore: bool, + cx: &mut ModelContext, + ) { + if let Some(session) = self.session_by_id(session_id) { + session.update(cx, |session, cx| { + session.set_ignore_breakpoints(ignore, cx); + }); + } + } + + pub fn ignore_breakpoints(&self, session_id: &DebugSessionId, cx: &AppContext) -> bool { self.session_by_id(session_id) .map(|session| session.read(cx).ignore_breakpoints()) .unwrap_or_default() @@ -516,13 +648,15 @@ impl DapStore { let session = store.session_by_id(&session_id).unwrap(); session.update(cx, |session, cx| { - session.update_configuration( + let local_session = session.as_local_mut().unwrap(); + + local_session.update_configuration( |old_config| { *old_config = config.clone(); }, cx, ); - session.add_client(Arc::new(client), cx); + local_session.add_client(Arc::new(client), cx); }); // don't emit this event ourself in tests, so we can add request, @@ -658,7 +792,7 @@ impl DapStore { self.start_client_internal(session_id, worktree_id, config.clone(), cx); cx.spawn(|this, mut cx| async move { - let session = cx.new_model(|_| DebugSession::new(session_id, config))?; + let session = cx.new_model(|_| DebugSession::new_local(session_id, config))?; let client = match start_client_task.await { Ok(client) => client, @@ -674,7 +808,10 @@ impl DapStore { this.update(&mut cx, |store, cx| { session.update(cx, |session, cx| { - session.add_client(client.clone(), cx); + session + .as_local_mut() + .unwrap() + .add_client(client.clone(), cx); }); let client_id = client.id(); @@ -750,7 +887,7 @@ impl DapStore { ))); }; - let config = session.read(cx).configuration(); + let config = session.read(cx).as_local().unwrap().configuration(); let mut adapter_args = client.adapter().request_args(&config); if let Some(args) = config.initialize_args.clone() { merge_json_value_into(args, &mut adapter_args); @@ -797,7 +934,7 @@ impl DapStore { // comes in we send another `attach` request with the already selected PID // If we don't do this the user has to select the process twice if the adapter sends a `startDebugging` request session.update(cx, |session, cx| { - session.update_configuration( + session.as_local_mut().unwrap().update_configuration( |config| { config.request = DebugRequestType::Attach(task::AttachConfig { process_id: Some(process_id), @@ -807,7 +944,7 @@ impl DapStore { ); }); - let config = session.read(cx).configuration(); + let config = session.read(cx).as_local().unwrap().configuration(); let mut adapter_args = client.adapter().request_args(&config); if let Some(args) = config.initialize_args.clone() { @@ -974,8 +1111,15 @@ impl DapStore { ))); }; + let Some(config) = session + .read(cx) + .as_local() + .map(|session| session.configuration()) + else { + return Task::ready(Err(anyhow!("Cannot find debug session: {:?}", session_id))); + }; + let session_id = *session_id; - let config = session.read(cx).configuration().clone(); let request_args = args.unwrap_or_else(|| StartDebuggingRequestArguments { configuration: config.initialize_args.clone().unwrap_or_default(), @@ -986,17 +1130,17 @@ impl DapStore { }); // Merge the new configuration over the existing configuration - let mut initialize_args = config.initialize_args.unwrap_or_default(); + let mut initialize_args = config.initialize_args.clone().unwrap_or_default(); merge_json_value_into(request_args.configuration, &mut initialize_args); let new_config = DebugAdapterConfig { label: config.label.clone(), kind: config.kind.clone(), - request: match request_args.request { + request: match &request_args.request { StartDebuggingRequestArgumentsRequest::Launch => DebugRequestType::Launch, StartDebuggingRequestArgumentsRequest::Attach => DebugRequestType::Attach( - if let DebugRequestType::Attach(attach_config) = config.request { - attach_config + if let DebugRequestType::Attach(attach_config) = &config.request { + attach_config.clone() } else { AttachConfig::default() }, @@ -1534,11 +1678,27 @@ impl DapStore { return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); }; + let Some(local_session) = session.read(cx).as_local() else { + return Task::ready(Err(anyhow!( + "Cannot shutdown session on remote side: {:?}", + session_id + ))); + }; + let mut tasks = Vec::new(); - for client in session.read(cx).clients().collect::>() { + for client in local_session.clients().collect::>() { tasks.push(self.shutdown_client(&session, client, cx)); } + if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { + downstream_client + .send(proto::DebuggerSessionEnded { + project_id: *project_id, + session_id: session_id.to_proto(), + }) + .log_err(); + } + cx.background_executor().spawn(async move { futures::future::join_all(tasks).await; Ok(()) @@ -1600,11 +1760,13 @@ impl DapStore { debug_sessions: Vec, cx: &mut ModelContext, ) { - for (session_id, debug_clients) in debug_sessions - .into_iter() - .map(|session| (session.session_id, session.clients)) - { - for debug_client in debug_clients { + for session in debug_sessions.into_iter() { + let session_id = DebugSessionId::from_proto(session.session_id); + let ignore_breakpoints = Some(session.ignore_breakpoints); + + self.add_remote_session(session_id, ignore_breakpoints, cx); + + for debug_client in session.clients { if let DapStoreMode::Remote(remote) = &mut self.mode { if let Some(queue) = &mut remote.event_queue { debug_client.debug_panel_items.into_iter().for_each(|item| { @@ -1613,9 +1775,13 @@ impl DapStore { } } + let client = DebugAdapterClientId::from_proto(debug_client.client_id); + + self.add_client_to_session(session_id, client); + self.update_capabilities_for_client( - &DebugSessionId::from_proto(session_id), - &DebugAdapterClientId::from_proto(debug_client.client_id), + &session_id, + &client, &dap::proto_conversions::capabilities_from_proto( &debug_client.capabilities.unwrap_or_default(), ), @@ -1652,7 +1818,7 @@ impl DapStore { cx.notify(); } - async fn handle_shutdown_session( + async fn handle_shutdown_session_request( this: Model, envelope: TypedEnvelope, mut cx: AsyncAppContext, @@ -1940,8 +2106,11 @@ impl DapStore { .collect::>(); let mut tasks = Vec::new(); - for session in local_store.sessions.values() { - let session = session.read(cx); + for session in local_store + .sessions + .values() + .filter_map(|session| session.read(cx).as_local()) + { let ignore_breakpoints = self.ignore_breakpoints(&session.id(), cx); for client in session.clients().collect::>() { tasks.push(self.send_breakpoints( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1897d95e6126a0..99a1a915e909fd 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -633,6 +633,8 @@ impl Project { client.add_model_request_handler(WorktreeStore::handle_rename_project_entry); + client.add_model_message_handler(Self::handle_toggle_ignore_breakpoints); + WorktreeStore::init(&client); BufferStore::init(&client); LspStore::init(&client); @@ -1398,6 +1400,26 @@ impl Project { result } + async fn handle_toggle_ignore_breakpoints( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |project, cx| { + // Only the host should handle this message because the host + // handles direct communication with the debugger servers. + if let Some((_, _)) = project.dap_store.read(cx).downstream_client() { + project + .toggle_ignore_breakpoints( + &DebugSessionId::from_proto(envelope.payload.session_id), + &DebugAdapterClientId::from_proto(envelope.payload.client_id), + cx, + ) + .detach_and_log_err(cx); + } + }) + } + pub fn toggle_ignore_breakpoints( &self, session_id: &DebugSessionId, @@ -1405,8 +1427,30 @@ impl Project { cx: &mut ModelContext, ) -> Task> { let tasks = self.dap_store.update(cx, |store, cx| { + if let Some((upstream_client, project_id)) = store.upstream_client() { + upstream_client + .send(proto::ToggleIgnoreBreakpoints { + session_id: session_id.to_proto(), + client_id: client_id.to_proto(), + project_id, + }) + .log_err(); + + return Vec::new(); + } + store.toggle_ignore_breakpoints(session_id, cx); + if let Some((downstream_client, project_id)) = store.downstream_client() { + downstream_client + .send(proto::IgnoreBreakpointState { + session_id: session_id.to_proto(), + project_id: *project_id, + ignore: store.ignore_breakpoints(session_id, cx), + }) + .log_err(); + } + let mut tasks = Vec::new(); for (project_path, breakpoints) in store.breakpoints() { diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 52426ac69e882d..b6e6690d601b3a 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -331,7 +331,10 @@ message Envelope { UpdateThreadStatus update_thread_status = 310; VariablesRequest variables_request = 311; DapVariables dap_variables = 312; - DapRestartStackFrameRequest dap_restart_stack_frame_request = 313; // current max + DapRestartStackFrameRequest dap_restart_stack_frame_request = 313; + IgnoreBreakpointState ignore_breakpoint_state = 314; + ToggleIgnoreBreakpoints toggle_ignore_breakpoints = 315; + DebuggerSessionEnded debugger_session_ended = 316; // current max } reserved 87 to 88; @@ -2471,10 +2474,16 @@ enum BreakpointKind { Log = 1; } +message DebuggerSessionEnded { + uint64 project_id = 1; + uint64 session_id = 2; +} + message DebuggerSession { uint64 session_id = 1; uint64 project_id = 2; - repeated DebugClient clients = 3; + bool ignore_breakpoints = 3; + repeated DebugClient clients = 4; } message DebugClient { @@ -2709,6 +2718,18 @@ message DapShutdownSession { optional uint64 session_id = 2; // Shutdown all sessions if this is None } +message ToggleIgnoreBreakpoints { + uint64 project_id = 1; + uint64 client_id = 2; + uint64 session_id = 3; +} + +message IgnoreBreakpointState { + uint64 project_id = 1; + uint64 session_id = 2; + bool ignore = 3; +} + message DapNextRequest { uint64 project_id = 1; uint64 client_id = 2; diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 0af3cbaf4cb548..a79bcf08183941 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -397,6 +397,9 @@ messages!( (UsersResponse, Foreground), (VariablesRequest, Background), (DapVariables, Background), + (IgnoreBreakpointState, Background), + (ToggleIgnoreBreakpoints, Background), + (DebuggerSessionEnded, Background), ); request_messages!( @@ -644,6 +647,9 @@ entity_messages!( DapShutdownSession, UpdateThreadStatus, VariablesRequest, + IgnoreBreakpointState, + ToggleIgnoreBreakpoints, + DebuggerSessionEnded, ); entity_messages!(