debugger_ui: Show a toast when setting breakpoints fails (#28815)

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>
This commit is contained in:
Cole Miller 2025-04-17 18:10:57 -04:00 committed by GitHub
parent 80a2f71d8e
commit 4095011af5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 101 additions and 32 deletions

View file

@ -12,7 +12,7 @@ use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self},
};
use std::{hash::Hash, ops::Range, path::Path, sync::Arc};
use std::{hash::Hash, ops::Range, path::Path, sync::Arc, u32};
use text::{Point, PointUtf16};
use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};

View file

@ -23,7 +23,7 @@ use futures::{
};
use gpui::{
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
Task,
Task, WeakEntity,
};
use http_client::HttpClient;
use language::{BinaryStatus, LanguageRegistry, LanguageToolchainStore};
@ -320,7 +320,7 @@ impl DapStore {
session.set_ignore_breakpoints(envelope.payload.ignore, cx)
})
} else {
Task::ready(())
Task::ready(HashMap::default())
}
})?
.await;
@ -350,6 +350,7 @@ impl DapStore {
&mut self,
binary: DebugAdapterBinary,
config: DebugTaskDefinition,
worktree: WeakEntity<Worktree>,
parent_session: Option<Entity<Session>>,
cx: &mut Context<Self>,
) -> (SessionId, Task<Result<Entity<Session>>>) {
@ -373,6 +374,7 @@ impl DapStore {
let start_client_task = this.update(cx, |this, cx| {
Session::local(
this.breakpoint_store.clone(),
worktree.clone(),
session_id,
parent_session,
binary,
@ -385,7 +387,7 @@ impl DapStore {
let ret = this
.update(cx, |_, cx| {
create_new_session(session_id, initialized_rx, start_client_task, cx)
create_new_session(session_id, initialized_rx, start_client_task, worktree, cx)
})?
.await;
ret
@ -404,6 +406,16 @@ impl DapStore {
return Task::ready(Err(anyhow!("Session not found")));
};
let Some(worktree) = parent_session
.read(cx)
.as_local()
.map(|local| local.worktree().clone())
else {
return Task::ready(Err(anyhow!(
"Cannot handle start debugging request from remote end"
)));
};
let args = serde_json::from_value::<StartDebuggingRequestArguments>(
request.arguments.unwrap_or_default(),
)
@ -413,7 +425,7 @@ impl DapStore {
binary.request_args = args;
let new_session_task = self
.new_session(binary, config, Some(parent_session.clone()), cx)
.new_session(binary, config, worktree, Some(parent_session.clone()), cx)
.1;
let request_seq = request.seq;
@ -742,6 +754,7 @@ fn create_new_session(
session_id: SessionId,
initialized_rx: oneshot::Receiver<()>,
start_client_task: Task<Result<Entity<Session>, anyhow::Error>>,
worktree: WeakEntity<Worktree>,
cx: &mut Context<DapStore>,
) -> Task<Result<Entity<Session>>> {
let task = cx.spawn(async move |this, cx| {
@ -771,7 +784,7 @@ fn create_new_session(
session
.update(cx, |session, cx| {
session.initialize_sequence(initialized_rx, cx)
session.initialize_sequence(initialized_rx, this.clone(), cx)
})?
.await
};
@ -820,12 +833,13 @@ fn create_new_session(
let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
let worktree = worktree.clone();
cx.spawn(async move |this, cx| {
task.await;
this.update(cx, |this, cx| {
this.sessions.remove(&session_id);
this.new_session(binary, config, None, cx)
this.new_session(binary, config, worktree, None, cx)
})?
.1
.await?;

View file

@ -9,6 +9,7 @@ use super::dap_command::{
StepBackCommand, StepCommand, StepInCommand, StepOutCommand, TerminateCommand,
TerminateThreadsCommand, ThreadsCommand, VariablesCommand,
};
use super::dap_store::DapStore;
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet, IndexMap, IndexSet};
use dap::adapters::DebugAdapterBinary;
@ -26,6 +27,7 @@ use gpui::{
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
Task, WeakEntity,
};
use rpc::AnyProtoClient;
use serde_json::{Value, json};
use smol::stream::StreamExt;
@ -42,6 +44,7 @@ use std::{
use task::DebugTaskDefinition;
use text::{PointUtf16, ToPointUtf16};
use util::{ResultExt, merge_json_value_into};
use worktree::Worktree;
#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
#[repr(transparent)]
@ -163,6 +166,7 @@ pub struct LocalMode {
binary: DebugAdapterBinary,
pub(crate) breakpoint_store: Entity<BreakpointStore>,
tmp_breakpoint: Option<SourceBreakpoint>,
worktree: WeakEntity<Worktree>,
}
fn client_source(abs_path: &Path) -> dap::Source {
@ -184,6 +188,7 @@ impl LocalMode {
fn new(
session_id: SessionId,
parent_session: Option<Entity<Session>>,
worktree: WeakEntity<Worktree>,
breakpoint_store: Entity<BreakpointStore>,
config: DebugTaskDefinition,
binary: DebugAdapterBinary,
@ -194,6 +199,7 @@ impl LocalMode {
session_id,
parent_session,
breakpoint_store,
worktree,
config,
binary,
messages_tx,
@ -206,6 +212,7 @@ impl LocalMode {
session_id: SessionId,
parent_session: Option<Entity<Session>>,
breakpoint_store: Entity<BreakpointStore>,
worktree: WeakEntity<Worktree>,
config: DebugTaskDefinition,
binary: DebugAdapterBinary,
messages_tx: futures::channel::mpsc::UnboundedSender<Message>,
@ -240,6 +247,7 @@ impl LocalMode {
let mut session = Self {
client,
breakpoint_store,
worktree,
tmp_breakpoint: None,
definition: config,
binary,
@ -251,6 +259,10 @@ impl LocalMode {
})
}
pub(crate) fn worktree(&self) -> &WeakEntity<Worktree> {
&self.worktree
}
fn unset_breakpoints_from_paths(&self, paths: &Vec<Arc<Path>>, cx: &mut App) -> Task<()> {
let tasks: Vec<_> = paths
.into_iter()
@ -335,7 +347,12 @@ impl LocalMode {
};
self.request(arg, cx.background_executor().clone())
}
fn send_source_breakpoints(&self, ignore_breakpoints: bool, cx: &App) -> Task<()> {
fn send_source_breakpoints(
&self,
ignore_breakpoints: bool,
cx: &App,
) -> Task<HashMap<Arc<Path>, anyhow::Error>> {
let mut breakpoint_tasks = Vec::new();
let breakpoints = self
.breakpoint_store
@ -352,26 +369,25 @@ impl LocalMode {
.collect()
};
breakpoint_tasks.push(self.request(
dap_command::SetBreakpoints {
source: client_source(&path),
source_modified: Some(false),
breakpoints,
},
cx.background_executor().clone(),
));
breakpoint_tasks.push(
self.request(
dap_command::SetBreakpoints {
source: client_source(&path),
source_modified: Some(false),
breakpoints,
},
cx.background_executor().clone(),
)
.map(|result| result.map_err(|e| (path, e))),
);
}
cx.background_spawn(async move {
futures::future::join_all(breakpoint_tasks)
.await
.iter()
.for_each(|res| match res {
Ok(_) => {}
Err(err) => {
log::warn!("Set breakpoints request failed: {}", err);
}
});
.into_iter()
.filter_map(Result::err)
.collect::<HashMap<_, _>>()
})
}
@ -389,6 +405,7 @@ impl LocalMode {
&self,
capabilities: &Capabilities,
initialized_rx: oneshot::Receiver<()>,
dap_store: WeakEntity<DapStore>,
cx: &App,
) -> Task<Result<()>> {
let mut raw = self.binary.request_args.clone();
@ -431,12 +448,40 @@ impl LocalMode {
.unwrap_or_default();
let configuration_sequence = cx.spawn({
let this = self.clone();
let worktree = self.worktree().clone();
async move |cx| {
initialized_rx.await?;
// todo(debugger) figure out if we want to handle a breakpoint response error
// This will probably consist of letting a user know that breakpoints failed to be set
cx.update(|cx| this.send_source_breakpoints(false, cx))?
let errors_by_path = cx
.update(|cx| this.send_source_breakpoints(false, cx))?
.await;
dap_store.update(cx, |_, cx| {
let Some(worktree) = worktree.upgrade() else {
return;
};
for (path, error) in &errors_by_path {
log::error!("failed to set breakpoints for {path:?}: {error}");
}
if let Some(failed_path) = errors_by_path.keys().next() {
let failed_path = failed_path
.strip_prefix(worktree.read(cx).abs_path())
.unwrap_or(failed_path)
.display();
let message = format!(
"Failed to set breakpoints for {failed_path}{}",
match errors_by_path.len() {
0 => unreachable!(),
1 => "".into(),
2 => " and 1 other path".into(),
n => format!(" and {} other paths", n - 1),
}
);
cx.emit(super::dap_store::DapStoreEvent::Notification(message));
}
})?;
cx.update(|cx| {
this.send_exception_breakpoints(
exception_filters,
@ -699,6 +744,7 @@ impl EventEmitter<SessionStateEvent> for Session {}
impl Session {
pub(crate) fn local(
breakpoint_store: Entity<BreakpointStore>,
worktree: WeakEntity<Worktree>,
session_id: SessionId,
parent_session: Option<Entity<Session>>,
binary: DebugAdapterBinary,
@ -713,6 +759,7 @@ impl Session {
let mode = LocalMode::new(
session_id,
parent_session.clone(),
worktree,
breakpoint_store.clone(),
config.clone(),
binary,
@ -871,11 +918,12 @@ impl Session {
pub(super) fn initialize_sequence(
&mut self,
initialize_rx: oneshot::Receiver<()>,
dap_store: WeakEntity<DapStore>,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
match &self.mode {
Mode::Local(local_mode) => {
local_mode.initialize_sequence(&self.capabilities, initialize_rx, cx)
local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx)
}
Mode::Remote(_) => Task::ready(Err(anyhow!("cannot initialize remote session"))),
}
@ -1292,13 +1340,20 @@ impl Session {
self.ignore_breakpoints
}
pub fn toggle_ignore_breakpoints(&mut self, cx: &mut App) -> Task<()> {
pub fn toggle_ignore_breakpoints(
&mut self,
cx: &mut App,
) -> Task<HashMap<Arc<Path>, anyhow::Error>> {
self.set_ignore_breakpoints(!self.ignore_breakpoints, cx)
}
pub(crate) fn set_ignore_breakpoints(&mut self, ignore: bool, cx: &mut App) -> Task<()> {
pub(crate) fn set_ignore_breakpoints(
&mut self,
ignore: bool,
cx: &mut App,
) -> Task<HashMap<Arc<Path>, anyhow::Error>> {
if self.ignore_breakpoints == ignore {
return Task::ready(());
return Task::ready(HashMap::default());
}
self.ignore_breakpoints = ignore;

View file

@ -1462,7 +1462,7 @@ impl Project {
config: DebugTaskDefinition,
cx: &mut Context<Self>,
) -> Task<Result<Entity<Session>>> {
let Some(worktree) = self.worktrees(cx).next() else {
let Some(worktree) = self.worktrees(cx).find(|tree| tree.read(cx).is_visible()) else {
return Task::ready(Err(anyhow!("Failed to find a worktree")));
};
@ -1501,7 +1501,7 @@ impl Project {
let ret = this
.update(cx, |project, cx| {
project.dap_store.update(cx, |dap_store, cx| {
dap_store.new_session(binary, config, None, cx)
dap_store.new_session(binary, config, worktree.downgrade(), None, cx)
})
})?
.1