From ed7360206fba88d857a63ac9d88d131e3f2a9fc6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 31 Dec 2025 19:40:11 +0100 Subject: [PATCH] debugger: Fix breakpoint store RPC handlers not being registered correctly on SSH remotes (#44908) Closes #36789 Release Notes: - Fixed setting breakpoints on remotes --------- Co-authored-by: Zed AI --- .../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(-) diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs index d5a144ee8a83e0064b3afdca123c33e4e8da3e46..15c3a2586ed222a169c8f3140b04247b08add9e6 100644 --- a/crates/project/src/debugger/breakpoint_store.rs +++ b/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, - buffer_store: Entity, + 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, + worktree_store: Entity, breakpoints: BTreeMap, BreakpointsInFile>, downstream_client: Option<(AnyProtoClient, u64)>, active_stack_frame: Option, @@ -170,28 +166,34 @@ impl BreakpointStore { pub fn local(worktree_store: Entity, buffer_store: Entity) -> 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, + worktree_store: Entity, + ) -> 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, + this: Entity, message: TypedEnvelope, 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::::from(message.payload.path.as_ref())) @@ -263,19 +269,20 @@ impl BreakpointStore { } async fn handle_toggle_breakpoint( - this: Entity, + this: Entity, message: TypedEnvelope, mut cx: AsyncApp, ) -> Result { - 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, Vec>, cx: &mut Context, ) -> Task> { - 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(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 927f6b7fe9a1a151bbf99925f8a0707ad1ff2d4e..6755d5df7ee5f2f1244ea75ff90e5dbd9bf6543e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -832,6 +832,7 @@ enum EntitySubscription { LspStore(PendingEntitySubscription), SettingsObserver(PendingEntitySubscription), DapStore(PendingEntitySubscription), + BreakpointStore(PendingEntitySubscription), } #[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::(remote_id)?, ), EntitySubscription::DapStore(client.subscribe_to_entity::(remote_id)?), + EntitySubscription::BreakpointStore( + client.subscribe_to_entity::(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, - subscriptions: [EntitySubscription; 7], + subscriptions: [EntitySubscription; 8], client: Arc, run_tasks: bool, user_store: Entity, @@ -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::>(); @@ -4580,11 +4601,9 @@ impl Project { } pub fn project_path_for_absolute_path(&self, abs_path: &Path, cx: &App) -> Option { - 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 { diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index c2c3519c4ac744a5751e6d24715ea0cc06ff5b52..a26740159d0a046a13b2927df0b0555fd0a048a3 100644 --- a/crates/project/src/worktree_store.rs +++ b/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 { + 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 { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; Some(worktree.read(cx).absolutize(&project_path.path)) diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index e6724beb3535f6097790ebef48e68d1331d85308..a3e644843d37f3425d4a5692733bf4518603fd4d 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -53,6 +53,7 @@ pub struct HeadlessProject { pub lsp_store: Entity, pub task_store: Entity, pub dap_store: Entity, + pub breakpoint_store: Entity, pub agent_server_store: Entity, pub settings_observer: Entity, pub next_entry_id: Arc, @@ -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,