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 <ai@zed.dev>
This commit is contained in:
Piotr Osiewicz
2025-12-31 19:40:11 +01:00
committed by GitHub
parent 9f599466b5
commit ed7360206f
4 changed files with 89 additions and 48 deletions

View File

@@ -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(),

View File

@@ -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> {

View File

@@ -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))

View File

@@ -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,