debugger: Fix breakpoint store RPC handlers not being registered correctly on SSH remotes (#44908)

Piotr Osiewicz and Zed AI created

Closes #36789

Release Notes:

- Fixed setting breakpoints on remotes

---------

Co-authored-by: Zed AI <ai@zed.dev>

Change summary

crates/project/src/debugger/breakpoint_store.rs | 74 ++++++++++--------
crates/project/src/project.rs                   | 41 +++++++--
crates/project/src/worktree_store.rs            |  8 ++
crates/remote_server/src/headless_project.rs    | 14 ++
4 files changed, 89 insertions(+), 48 deletions(-)

Detailed changes

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

@@ -17,7 +17,7 @@ use std::{hash::Hash, ops::Range, path::Path, sync::Arc, u32};
 use text::{Point, PointUtf16};
 use util::maybe;
 
-use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
+use crate::{ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
 
 use super::session::ThreadId;
 
@@ -130,18 +130,12 @@ mod breakpoints_in_file {
 #[derive(Clone)]
 struct RemoteBreakpointStore {
     upstream_client: AnyProtoClient,
-    _upstream_project_id: u64,
-}
-
-#[derive(Clone)]
-struct LocalBreakpointStore {
-    worktree_store: Entity<WorktreeStore>,
-    buffer_store: Entity<BufferStore>,
+    upstream_project_id: u64,
 }
 
 #[derive(Clone)]
 enum BreakpointStoreMode {
-    Local(LocalBreakpointStore),
+    Local,
     Remote(RemoteBreakpointStore),
 }
 
@@ -155,6 +149,8 @@ pub struct ActiveStackFrame {
 }
 
 pub struct BreakpointStore {
+    buffer_store: Entity<BufferStore>,
+    worktree_store: Entity<WorktreeStore>,
     breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,
     downstream_client: Option<(AnyProtoClient, u64)>,
     active_stack_frame: Option<ActiveStackFrame>,
@@ -170,28 +166,34 @@ impl BreakpointStore {
     pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
         BreakpointStore {
             breakpoints: BTreeMap::new(),
-            mode: BreakpointStoreMode::Local(LocalBreakpointStore {
-                worktree_store,
-                buffer_store,
-            }),
+            mode: BreakpointStoreMode::Local,
+            buffer_store,
+            worktree_store,
             downstream_client: None,
             active_stack_frame: Default::default(),
         }
     }
 
-    pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
+    pub(crate) fn remote(
+        upstream_project_id: u64,
+        upstream_client: AnyProtoClient,
+        buffer_store: Entity<BufferStore>,
+        worktree_store: Entity<WorktreeStore>,
+    ) -> Self {
         BreakpointStore {
             breakpoints: BTreeMap::new(),
             mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
                 upstream_client,
-                _upstream_project_id: upstream_project_id,
+                upstream_project_id,
             }),
+            buffer_store,
+            worktree_store,
             downstream_client: None,
             active_stack_frame: Default::default(),
         }
     }
 
-    pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
+    pub fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
         self.downstream_client = Some((downstream_client, project_id));
     }
 
@@ -202,27 +204,31 @@ impl BreakpointStore {
     }
 
     async fn handle_breakpoints_for_file(
-        this: Entity<Project>,
+        this: Entity<Self>,
         message: TypedEnvelope<proto::BreakpointsForFile>,
         mut cx: AsyncApp,
     ) -> Result<()> {
-        let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
         if message.payload.breakpoints.is_empty() {
             return Ok(());
         }
 
         let buffer = this
             .update(&mut cx, |this, cx| {
-                let path =
-                    this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
-                Some(this.open_buffer(path, cx))
+                let path = this
+                    .worktree_store
+                    .read(cx)
+                    .project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
+                Some(
+                    this.buffer_store
+                        .update(cx, |this, cx| this.open_buffer(path, cx)),
+                )
             })
             .ok()
             .flatten()
             .context("Invalid project path")?
             .await?;
 
-        breakpoints.update(&mut cx, move |this, cx| {
+        this.update(&mut cx, move |this, cx| {
             let bps = this
                 .breakpoints
                 .entry(Arc::<Path>::from(message.payload.path.as_ref()))
@@ -263,19 +269,20 @@ impl BreakpointStore {
     }
 
     async fn handle_toggle_breakpoint(
-        this: Entity<Project>,
+        this: Entity<Self>,
         message: TypedEnvelope<proto::ToggleBreakpoint>,
         mut cx: AsyncApp,
     ) -> Result<proto::Ack> {
-        let breakpoints = this.read_with(&cx, |this, _| this.breakpoint_store())?;
         let path = this
             .update(&mut cx, |this, cx| {
-                this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)
+                this.worktree_store
+                    .read(cx)
+                    .project_path_for_absolute_path(message.payload.path.as_ref(), cx)
             })?
             .context("Could not resolve provided abs path")?;
         let buffer = this
             .update(&mut cx, |this, cx| {
-                this.buffer_store().read(cx).get_by_path(&path)
+                this.buffer_store.read(cx).get_by_path(&path)
             })?
             .context("Could not find buffer for a given path")?;
         let breakpoint = message
@@ -292,7 +299,7 @@ impl BreakpointStore {
         let breakpoint =
             Breakpoint::from_proto(breakpoint).context("Could not deserialize breakpoint")?;
 
-        breakpoints.update(&mut cx, |this, cx| {
+        this.update(&mut cx, |this, cx| {
             this.toggle_breakpoint(
                 buffer,
                 BreakpointWithPosition {
@@ -547,7 +554,7 @@ impl BreakpointStore {
                     .to_proto(&abs_path, &breakpoint.position, &HashMap::default())
             {
                 cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
-                    project_id: remote._upstream_project_id,
+                    project_id: remote.upstream_project_id,
                     path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
                     breakpoint: Some(breakpoint),
                 }))
@@ -775,22 +782,21 @@ impl BreakpointStore {
         breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
         cx: &mut Context<BreakpointStore>,
     ) -> Task<Result<()>> {
-        if let BreakpointStoreMode::Local(mode) = &self.mode {
-            let mode = mode.clone();
+        if let BreakpointStoreMode::Local = &self.mode {
+            let worktree_store = self.worktree_store.downgrade();
+            let buffer_store = self.buffer_store.downgrade();
             cx.spawn(async move |this, cx| {
                 let mut new_breakpoints = BTreeMap::default();
                 for (path, bps) in breakpoints {
                     if bps.is_empty() {
                         continue;
                     }
-                    let (worktree, relative_path) = mode
-                        .worktree_store
+                    let (worktree, relative_path) = worktree_store
                         .update(cx, |this, cx| {
                             this.find_or_create_worktree(&path, false, cx)
                         })?
                         .await?;
-                    let buffer = mode
-                        .buffer_store
+                    let buffer = buffer_store
                         .update(cx, |this, cx| {
                             let path = ProjectPath {
                                 worktree_id: worktree.read(cx).id(),

crates/project/src/project.rs 🔗

@@ -832,6 +832,7 @@ enum EntitySubscription {
     LspStore(PendingEntitySubscription<LspStore>),
     SettingsObserver(PendingEntitySubscription<SettingsObserver>),
     DapStore(PendingEntitySubscription<DapStore>),
+    BreakpointStore(PendingEntitySubscription<BreakpointStore>),
 }
 
 #[derive(Debug, Clone)]
@@ -1378,8 +1379,14 @@ impl Project {
             });
             cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
 
-            let breakpoint_store =
-                cx.new(|_| BreakpointStore::remote(REMOTE_SERVER_PROJECT_ID, remote_proto.clone()));
+            let breakpoint_store = cx.new(|_| {
+                BreakpointStore::remote(
+                    REMOTE_SERVER_PROJECT_ID,
+                    remote_proto.clone(),
+                    buffer_store.clone(),
+                    worktree_store.clone(),
+                )
+            });
 
             let dap_store = cx.new(|cx| {
                 DapStore::new_remote(
@@ -1475,6 +1482,7 @@ impl Project {
             remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.worktree_store);
             remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.lsp_store);
             remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.dap_store);
+            remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.breakpoint_store);
             remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.settings_observer);
             remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.git_store);
             remote_proto.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &this.agent_server_store);
@@ -1496,6 +1504,7 @@ impl Project {
             TaskStore::init(Some(&remote_proto));
             ToolchainStore::init(&remote_proto);
             DapStore::init(&remote_proto, cx);
+            BreakpointStore::init(&remote_proto);
             GitStore::init(&remote_proto);
             AgentServerStore::init_remote(&remote_proto);
 
@@ -1525,6 +1534,9 @@ impl Project {
                 client.subscribe_to_entity::<SettingsObserver>(remote_id)?,
             ),
             EntitySubscription::DapStore(client.subscribe_to_entity::<DapStore>(remote_id)?),
+            EntitySubscription::BreakpointStore(
+                client.subscribe_to_entity::<BreakpointStore>(remote_id)?,
+            ),
         ];
         let committer = get_git_committer(&cx).await;
         let response = client
@@ -1549,7 +1561,7 @@ impl Project {
 
     async fn from_join_project_response(
         response: TypedEnvelope<proto::JoinProjectResponse>,
-        subscriptions: [EntitySubscription; 7],
+        subscriptions: [EntitySubscription; 8],
         client: Arc<Client>,
         run_tasks: bool,
         user_store: Entity<UserStore>,
@@ -1583,8 +1595,14 @@ impl Project {
 
         let environment =
             cx.new(|cx| ProjectEnvironment::new(None, worktree_store.downgrade(), None, true, cx))?;
-        let breakpoint_store =
-            cx.new(|_| BreakpointStore::remote(remote_id, client.clone().into()))?;
+        let breakpoint_store = cx.new(|_| {
+            BreakpointStore::remote(
+                remote_id,
+                client.clone().into(),
+                buffer_store.clone(),
+                worktree_store.clone(),
+            )
+        })?;
         let dap_store = cx.new(|cx| {
             DapStore::new_collab(
                 remote_id,
@@ -1707,7 +1725,7 @@ impl Project {
                     remote_id,
                     replica_id,
                 },
-                breakpoint_store,
+                breakpoint_store: breakpoint_store.clone(),
                 dap_store: dap_store.clone(),
                 git_store: git_store.clone(),
                 agent_server_store,
@@ -1766,6 +1784,9 @@ impl Project {
                 EntitySubscription::DapStore(subscription) => {
                     subscription.set_entity(&dap_store, &cx)
                 }
+                EntitySubscription::BreakpointStore(subscription) => {
+                    subscription.set_entity(&breakpoint_store, &cx)
+                }
             })
             .collect::<Vec<_>>();
 
@@ -4580,11 +4601,9 @@ impl Project {
     }
 
     pub fn project_path_for_absolute_path(&self, abs_path: &Path, cx: &App) -> Option<ProjectPath> {
-        self.find_worktree(abs_path, cx)
-            .map(|(worktree, relative_path)| ProjectPath {
-                worktree_id: worktree.read(cx).id(),
-                path: relative_path,
-            })
+        self.worktree_store
+            .read(cx)
+            .project_path_for_absolute_path(abs_path, cx)
     }
 
     pub fn get_workspace_root(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {

crates/project/src/worktree_store.rs 🔗

@@ -169,6 +169,14 @@ impl WorktreeStore {
         None
     }
 
+    pub fn project_path_for_absolute_path(&self, abs_path: &Path, cx: &App) -> Option<ProjectPath> {
+        self.find_worktree(abs_path, cx)
+            .map(|(worktree, relative_path)| ProjectPath {
+                worktree_id: worktree.read(cx).id(),
+                path: relative_path,
+            })
+    }
+
     pub fn absolutize(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
         let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
         Some(worktree.read(cx).absolutize(&project_path.path))

crates/remote_server/src/headless_project.rs 🔗

@@ -53,6 +53,7 @@ pub struct HeadlessProject {
     pub lsp_store: Entity<LspStore>,
     pub task_store: Entity<TaskStore>,
     pub dap_store: Entity<DapStore>,
+    pub breakpoint_store: Entity<BreakpointStore>,
     pub agent_server_store: Entity<AgentServerStore>,
     pub settings_observer: Entity<SettingsObserver>,
     pub next_entry_id: Arc<AtomicUsize>,
@@ -131,8 +132,13 @@ impl HeadlessProject {
             buffer_store
         });
 
-        let breakpoint_store =
-            cx.new(|_| BreakpointStore::local(worktree_store.clone(), buffer_store.clone()));
+        let breakpoint_store = cx.new(|_| {
+            let mut breakpoint_store =
+                BreakpointStore::local(worktree_store.clone(), buffer_store.clone());
+            breakpoint_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone());
+
+            breakpoint_store
+        });
 
         let dap_store = cx.new(|cx| {
             let mut dap_store = DapStore::new_local(
@@ -258,6 +264,7 @@ impl HeadlessProject {
         session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store);
         session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store);
         session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store);
+        session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &breakpoint_store);
         session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer);
         session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store);
         session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &agent_server_store);
@@ -301,7 +308,7 @@ impl HeadlessProject {
         ToolchainStore::init(&session);
         DapStore::init(&session, cx);
         // todo(debugger): Re init breakpoint store when we set it up for collab
-        // BreakpointStore::init(&client);
+        BreakpointStore::init(&session);
         GitStore::init(&session);
         AgentServerStore::init_headless(&session);
 
@@ -315,6 +322,7 @@ impl HeadlessProject {
             lsp_store,
             task_store,
             dap_store,
+            breakpoint_store,
             agent_server_store,
             languages,
             extensions,