1//! Module for managing breakpoints in a project.
2//!
3//! Breakpoints are separate from a session because they're not associated with any particular debug session. They can also be set up without a session running.
4use anyhow::{anyhow, Result};
5use breakpoints_in_file::BreakpointsInFile;
6use collections::BTreeMap;
7use dap::client::SessionId;
8use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Task};
9use language::{proto::serialize_anchor as serialize_text_anchor, Buffer, BufferSnapshot};
10use rpc::{
11 proto::{self},
12 AnyProtoClient, TypedEnvelope,
13};
14use std::{
15 hash::{Hash, Hasher},
16 ops::Range,
17 path::Path,
18 sync::Arc,
19};
20use text::PointUtf16;
21
22use crate::{buffer_store::BufferStore, worktree_store::WorktreeStore, Project, ProjectPath};
23
24mod breakpoints_in_file {
25 use language::BufferEvent;
26
27 use super::*;
28
29 #[derive(Clone)]
30 pub(super) struct BreakpointsInFile {
31 pub(super) buffer: Entity<Buffer>,
32 // TODO: This is.. less than ideal, as it's O(n) and does not return entries in order. We'll have to change TreeMap to support passing in the context for comparisons
33 pub(super) breakpoints: Vec<(text::Anchor, Breakpoint)>,
34 _subscription: Arc<gpui::Subscription>,
35 }
36
37 impl BreakpointsInFile {
38 pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {
39 let subscription =
40 Arc::from(cx.subscribe(&buffer, |_, buffer, event, cx| match event {
41 BufferEvent::Saved => {
42 if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
43 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
44 abs_path,
45 BreakpointUpdatedReason::FileSaved,
46 ));
47 }
48 }
49 _ => {}
50 }));
51
52 BreakpointsInFile {
53 buffer,
54 breakpoints: Vec::new(),
55 _subscription: subscription,
56 }
57 }
58 }
59}
60
61#[derive(Clone)]
62struct RemoteBreakpointStore {
63 upstream_client: AnyProtoClient,
64 _upstream_project_id: u64,
65}
66
67#[derive(Clone)]
68struct LocalBreakpointStore {
69 worktree_store: Entity<WorktreeStore>,
70 buffer_store: Entity<BufferStore>,
71}
72
73#[derive(Clone)]
74enum BreakpointStoreMode {
75 Local(LocalBreakpointStore),
76 Remote(RemoteBreakpointStore),
77}
78pub struct BreakpointStore {
79 breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,
80 downstream_client: Option<(AnyProtoClient, u64)>,
81 active_stack_frame: Option<(SessionId, Arc<Path>, text::Anchor)>,
82 // E.g ssh
83 mode: BreakpointStoreMode,
84}
85
86impl BreakpointStore {
87 pub fn init(client: &AnyProtoClient) {
88 client.add_entity_request_handler(Self::handle_toggle_breakpoint);
89 client.add_entity_message_handler(Self::handle_breakpoints_for_file);
90 }
91 pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
92 BreakpointStore {
93 breakpoints: BTreeMap::new(),
94 mode: BreakpointStoreMode::Local(LocalBreakpointStore {
95 worktree_store,
96 buffer_store,
97 }),
98 downstream_client: None,
99 active_stack_frame: Default::default(),
100 }
101 }
102
103 pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
104 BreakpointStore {
105 breakpoints: BTreeMap::new(),
106 mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
107 upstream_client,
108 _upstream_project_id: upstream_project_id,
109 }),
110 downstream_client: None,
111 active_stack_frame: Default::default(),
112 }
113 }
114
115 pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
116 self.downstream_client = Some((downstream_client.clone(), project_id));
117 }
118
119 pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) {
120 self.downstream_client.take();
121
122 cx.notify();
123 }
124
125 async fn handle_breakpoints_for_file(
126 this: Entity<Project>,
127 message: TypedEnvelope<proto::BreakpointsForFile>,
128 mut cx: AsyncApp,
129 ) -> Result<()> {
130 let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
131 if message.payload.breakpoints.is_empty() {
132 return Ok(());
133 }
134
135 let buffer = this
136 .update(&mut cx, |this, cx| {
137 let path =
138 this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
139 Some(this.open_buffer(path, cx))
140 })
141 .ok()
142 .flatten()
143 .ok_or_else(|| anyhow!("Invalid project path"))?
144 .await?;
145
146 breakpoints.update(&mut cx, move |this, cx| {
147 let bps = this
148 .breakpoints
149 .entry(Arc::<Path>::from(message.payload.path.as_ref()))
150 .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
151
152 bps.breakpoints = message
153 .payload
154 .breakpoints
155 .into_iter()
156 .filter_map(|breakpoint| {
157 let anchor = language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
158 let breakpoint = Breakpoint::from_proto(breakpoint)?;
159 Some((anchor, breakpoint))
160 })
161 .collect();
162
163 cx.notify();
164 })?;
165
166 Ok(())
167 }
168
169 async fn handle_toggle_breakpoint(
170 this: Entity<Project>,
171 message: TypedEnvelope<proto::ToggleBreakpoint>,
172 mut cx: AsyncApp,
173 ) -> Result<proto::Ack> {
174 let breakpoints = this.update(&mut cx, |this, _| this.breakpoint_store())?;
175 let path = this
176 .update(&mut cx, |this, cx| {
177 this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)
178 })?
179 .ok_or_else(|| anyhow!("Could not resolve provided abs path"))?;
180 let buffer = this
181 .update(&mut cx, |this, cx| {
182 this.buffer_store().read(cx).get_by_path(&path, cx)
183 })?
184 .ok_or_else(|| anyhow!("Could not find buffer for a given path"))?;
185 let breakpoint = message
186 .payload
187 .breakpoint
188 .ok_or_else(|| anyhow!("Breakpoint not present in RPC payload"))?;
189 let anchor = language::proto::deserialize_anchor(
190 breakpoint
191 .position
192 .clone()
193 .ok_or_else(|| anyhow!("Anchor not present in RPC payload"))?,
194 )
195 .ok_or_else(|| anyhow!("Anchor deserialization failed"))?;
196 let breakpoint = Breakpoint::from_proto(breakpoint)
197 .ok_or_else(|| anyhow!("Could not deserialize breakpoint"))?;
198
199 breakpoints.update(&mut cx, |this, cx| {
200 this.toggle_breakpoint(
201 buffer,
202 (anchor, breakpoint),
203 BreakpointEditAction::Toggle,
204 cx,
205 );
206 })?;
207 Ok(proto::Ack {})
208 }
209
210 pub(crate) fn broadcast(&self) {
211 if let Some((client, project_id)) = &self.downstream_client {
212 for (path, breakpoint_set) in &self.breakpoints {
213 let _ = client.send(proto::BreakpointsForFile {
214 project_id: *project_id,
215 path: path.to_str().map(ToOwned::to_owned).unwrap(),
216 breakpoints: breakpoint_set
217 .breakpoints
218 .iter()
219 .filter_map(|(anchor, bp)| bp.to_proto(&path, anchor))
220 .collect(),
221 });
222 }
223 }
224 }
225
226 fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
227 worktree::File::from_dyn(buffer.read(cx).file())
228 .and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
229 .map(Arc::<Path>::from)
230 }
231
232 pub fn toggle_breakpoint(
233 &mut self,
234 buffer: Entity<Buffer>,
235 mut breakpoint: (text::Anchor, Breakpoint),
236 edit_action: BreakpointEditAction,
237 cx: &mut Context<Self>,
238 ) {
239 let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
240 return;
241 };
242
243 let breakpoint_set = self
244 .breakpoints
245 .entry(abs_path.clone())
246 .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
247
248 match edit_action {
249 BreakpointEditAction::Toggle => {
250 let len_before = breakpoint_set.breakpoints.len();
251 breakpoint_set
252 .breakpoints
253 .retain(|value| &breakpoint != value);
254 if len_before == breakpoint_set.breakpoints.len() {
255 // We did not remove any breakpoint, hence let's toggle one.
256 breakpoint_set.breakpoints.push(breakpoint.clone());
257 }
258 }
259 BreakpointEditAction::EditLogMessage(log_message) => {
260 if !log_message.is_empty() {
261 breakpoint.1.kind = BreakpointKind::Log(log_message.clone());
262
263 let found_bp =
264 breakpoint_set
265 .breakpoints
266 .iter_mut()
267 .find_map(|(other_pos, other_bp)| {
268 if breakpoint.0 == *other_pos {
269 Some(other_bp)
270 } else {
271 None
272 }
273 });
274
275 if let Some(found_bp) = found_bp {
276 found_bp.kind = BreakpointKind::Log(log_message.clone());
277 } else {
278 // We did not remove any breakpoint, hence let's toggle one.
279 breakpoint_set.breakpoints.push(breakpoint.clone());
280 }
281 } else if matches!(&breakpoint.1.kind, BreakpointKind::Log(_)) {
282 breakpoint_set
283 .breakpoints
284 .retain(|(other_pos, other_kind)| {
285 &breakpoint.0 != other_pos
286 && matches!(other_kind.kind, BreakpointKind::Standard)
287 });
288 }
289 }
290 }
291
292 if breakpoint_set.breakpoints.is_empty() {
293 self.breakpoints.remove(&abs_path);
294 }
295 if let BreakpointStoreMode::Remote(remote) = &self.mode {
296 if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
297 cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
298 project_id: remote._upstream_project_id,
299 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
300 breakpoint: Some(breakpoint),
301 }))
302 .detach();
303 }
304 } else if let Some((client, project_id)) = &self.downstream_client {
305 let breakpoints = self
306 .breakpoints
307 .get(&abs_path)
308 .map(|breakpoint_set| {
309 breakpoint_set
310 .breakpoints
311 .iter()
312 .filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
313 .collect()
314 })
315 .unwrap_or_default();
316
317 let _ = client.send(proto::BreakpointsForFile {
318 project_id: *project_id,
319 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
320 breakpoints,
321 });
322 }
323
324 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
325 abs_path,
326 BreakpointUpdatedReason::Toggled,
327 ));
328 cx.notify();
329 }
330
331 pub fn on_file_rename(
332 &mut self,
333 old_path: Arc<Path>,
334 new_path: Arc<Path>,
335 cx: &mut Context<Self>,
336 ) {
337 if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
338 self.breakpoints.insert(new_path.clone(), breakpoints);
339
340 cx.notify();
341 }
342 }
343
344 pub fn breakpoints<'a>(
345 &'a self,
346 buffer: &'a Entity<Buffer>,
347 range: Option<Range<text::Anchor>>,
348 buffer_snapshot: BufferSnapshot,
349 cx: &App,
350 ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
351 let abs_path = Self::abs_path_from_buffer(buffer, cx);
352 abs_path
353 .and_then(|path| self.breakpoints.get(&path))
354 .into_iter()
355 .flat_map(move |file_breakpoints| {
356 file_breakpoints.breakpoints.iter().filter({
357 let range = range.clone();
358 let buffer_snapshot = buffer_snapshot.clone();
359 move |(position, _)| {
360 if let Some(range) = &range {
361 position.cmp(&range.start, &buffer_snapshot).is_ge()
362 && position.cmp(&range.end, &buffer_snapshot).is_le()
363 } else {
364 true
365 }
366 }
367 })
368 })
369 }
370
371 pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, text::Anchor)> {
372 self.active_stack_frame.as_ref()
373 }
374
375 pub fn remove_active_position(
376 &mut self,
377 session_id: Option<SessionId>,
378 cx: &mut Context<Self>,
379 ) {
380 if let Some(session_id) = session_id {
381 self.active_stack_frame
382 .take_if(|(id, _, _)| *id == session_id);
383 } else {
384 self.active_stack_frame.take();
385 }
386
387 cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
388 cx.notify();
389 }
390
391 pub fn set_active_position(
392 &mut self,
393 position: (SessionId, Arc<Path>, text::Anchor),
394 cx: &mut Context<Self>,
395 ) {
396 self.active_stack_frame = Some(position);
397 cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
398 cx.notify();
399 }
400
401 pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SerializedBreakpoint> {
402 self.breakpoints
403 .get(path)
404 .map(|bp| {
405 let snapshot = bp.buffer.read(cx).snapshot();
406 bp.breakpoints
407 .iter()
408 .map(|(position, breakpoint)| {
409 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
410 SerializedBreakpoint {
411 position,
412 path: path.clone(),
413 kind: breakpoint.kind.clone(),
414 }
415 })
416 .collect()
417 })
418 .unwrap_or_default()
419 }
420
421 pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> {
422 self.breakpoints
423 .iter()
424 .map(|(path, bp)| {
425 let snapshot = bp.buffer.read(cx).snapshot();
426 (
427 path.clone(),
428 bp.breakpoints
429 .iter()
430 .map(|(position, breakpoint)| {
431 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
432 SerializedBreakpoint {
433 position,
434 path: path.clone(),
435 kind: breakpoint.kind.clone(),
436 }
437 })
438 .collect(),
439 )
440 })
441 .collect()
442 }
443
444 pub fn with_serialized_breakpoints(
445 &self,
446 breakpoints: BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
447 cx: &mut Context<'_, BreakpointStore>,
448 ) -> Task<Result<()>> {
449 if let BreakpointStoreMode::Local(mode) = &self.mode {
450 let mode = mode.clone();
451 cx.spawn(move |this, mut cx| async move {
452 let mut new_breakpoints = BTreeMap::default();
453 for (path, bps) in breakpoints {
454 if bps.is_empty() {
455 continue;
456 }
457 let (worktree, relative_path) = mode
458 .worktree_store
459 .update(&mut cx, |this, cx| {
460 this.find_or_create_worktree(&path, false, cx)
461 })?
462 .await?;
463 let buffer = mode
464 .buffer_store
465 .update(&mut cx, |this, cx| {
466 let path = ProjectPath {
467 worktree_id: worktree.read(cx).id(),
468 path: relative_path.into(),
469 };
470 this.open_buffer(path, cx)
471 })?
472 .await;
473 let Ok(buffer) = buffer else {
474 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
475 continue;
476 };
477 let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
478
479 let mut breakpoints_for_file =
480 this.update(&mut cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
481
482 for bp in bps {
483 let position = snapshot.anchor_before(PointUtf16::new(bp.position, 0));
484 breakpoints_for_file
485 .breakpoints
486 .push((position, Breakpoint { kind: bp.kind }))
487 }
488 new_breakpoints.insert(path, breakpoints_for_file);
489 }
490 this.update(&mut cx, |this, cx| {
491 this.breakpoints = new_breakpoints;
492 cx.notify();
493 })?;
494
495 Ok(())
496 })
497 } else {
498 Task::ready(Ok(()))
499 }
500 }
501}
502
503#[derive(Clone, Copy)]
504pub enum BreakpointUpdatedReason {
505 Toggled,
506 FileSaved,
507}
508
509pub enum BreakpointStoreEvent {
510 ActiveDebugLineChanged,
511 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
512}
513
514impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
515
516type LogMessage = Arc<str>;
517
518#[derive(Clone, Debug)]
519pub enum BreakpointEditAction {
520 Toggle,
521 EditLogMessage(LogMessage),
522}
523
524#[derive(Clone, Debug)]
525pub enum BreakpointKind {
526 Standard,
527 Log(LogMessage),
528}
529
530impl BreakpointKind {
531 pub fn to_int(&self) -> i32 {
532 match self {
533 BreakpointKind::Standard => 0,
534 BreakpointKind::Log(_) => 1,
535 }
536 }
537
538 pub fn log_message(&self) -> Option<LogMessage> {
539 match self {
540 BreakpointKind::Standard => None,
541 BreakpointKind::Log(message) => Some(message.clone()),
542 }
543 }
544}
545
546impl PartialEq for BreakpointKind {
547 fn eq(&self, other: &Self) -> bool {
548 std::mem::discriminant(self) == std::mem::discriminant(other)
549 }
550}
551
552impl Eq for BreakpointKind {}
553
554impl Hash for BreakpointKind {
555 fn hash<H: Hasher>(&self, state: &mut H) {
556 std::mem::discriminant(self).hash(state);
557 }
558}
559
560#[derive(Clone, Debug, Hash, PartialEq, Eq)]
561pub struct Breakpoint {
562 pub kind: BreakpointKind,
563}
564
565impl Breakpoint {
566 fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
567 Some(client::proto::Breakpoint {
568 position: Some(serialize_text_anchor(position)),
569
570 kind: match self.kind {
571 BreakpointKind::Standard => proto::BreakpointKind::Standard.into(),
572 BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(),
573 },
574 message: if let BreakpointKind::Log(message) = &self.kind {
575 Some(message.to_string())
576 } else {
577 None
578 },
579 })
580 }
581
582 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
583 Some(Self {
584 kind: match proto::BreakpointKind::from_i32(breakpoint.kind) {
585 Some(proto::BreakpointKind::Log) => {
586 BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into())
587 }
588 None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard,
589 },
590 })
591 }
592}
593
594#[derive(Clone, Debug, Hash, PartialEq, Eq)]
595pub struct SerializedBreakpoint {
596 pub position: u32,
597 pub path: Arc<Path>,
598 pub kind: BreakpointKind,
599}
600
601impl From<SerializedBreakpoint> for dap::SourceBreakpoint {
602 fn from(bp: SerializedBreakpoint) -> Self {
603 Self {
604 line: bp.position as u64 + 1,
605 column: None,
606 condition: None,
607 hit_condition: None,
608 log_message: bp.kind.log_message().as_deref().map(Into::into),
609 mode: None,
610 }
611 }
612}