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, Subscription, 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<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::InvertState => {
260 if let Some((_, bp)) = breakpoint_set
261 .breakpoints
262 .iter_mut()
263 .find(|value| breakpoint == **value)
264 {
265 if bp.is_enabled() {
266 bp.state = BreakpointState::Disabled;
267 } else {
268 bp.state = BreakpointState::Enabled;
269 }
270 } else {
271 log::error!("Attempted to invert a breakpoint's state that doesn't exist ");
272 }
273 }
274 BreakpointEditAction::EditLogMessage(log_message) => {
275 if !log_message.is_empty() {
276 breakpoint.1.kind = BreakpointKind::Log(log_message.clone());
277
278 let found_bp =
279 breakpoint_set
280 .breakpoints
281 .iter_mut()
282 .find_map(|(other_pos, other_bp)| {
283 if breakpoint.0 == *other_pos {
284 Some(other_bp)
285 } else {
286 None
287 }
288 });
289
290 if let Some(found_bp) = found_bp {
291 found_bp.kind = BreakpointKind::Log(log_message.clone());
292 } else {
293 // We did not remove any breakpoint, hence let's toggle one.
294 breakpoint_set.breakpoints.push(breakpoint.clone());
295 }
296 } else if matches!(&breakpoint.1.kind, BreakpointKind::Log(_)) {
297 breakpoint_set
298 .breakpoints
299 .retain(|(other_pos, other_kind)| {
300 &breakpoint.0 != other_pos
301 && matches!(other_kind.kind, BreakpointKind::Standard)
302 });
303 }
304 }
305 }
306
307 if breakpoint_set.breakpoints.is_empty() {
308 self.breakpoints.remove(&abs_path);
309 }
310 if let BreakpointStoreMode::Remote(remote) = &self.mode {
311 if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
312 cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
313 project_id: remote._upstream_project_id,
314 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
315 breakpoint: Some(breakpoint),
316 }))
317 .detach();
318 }
319 } else if let Some((client, project_id)) = &self.downstream_client {
320 let breakpoints = self
321 .breakpoints
322 .get(&abs_path)
323 .map(|breakpoint_set| {
324 breakpoint_set
325 .breakpoints
326 .iter()
327 .filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
328 .collect()
329 })
330 .unwrap_or_default();
331
332 let _ = client.send(proto::BreakpointsForFile {
333 project_id: *project_id,
334 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
335 breakpoints,
336 });
337 }
338
339 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
340 abs_path,
341 BreakpointUpdatedReason::Toggled,
342 ));
343 cx.notify();
344 }
345
346 pub fn on_file_rename(
347 &mut self,
348 old_path: Arc<Path>,
349 new_path: Arc<Path>,
350 cx: &mut Context<Self>,
351 ) {
352 if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
353 self.breakpoints.insert(new_path.clone(), breakpoints);
354
355 cx.notify();
356 }
357 }
358
359 pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
360 let breakpoint_paths = self.breakpoints.keys().cloned().collect();
361 self.breakpoints.clear();
362 cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
363 }
364
365 pub fn breakpoints<'a>(
366 &'a self,
367 buffer: &'a Entity<Buffer>,
368 range: Option<Range<text::Anchor>>,
369 buffer_snapshot: &'a BufferSnapshot,
370 cx: &App,
371 ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
372 let abs_path = Self::abs_path_from_buffer(buffer, cx);
373 abs_path
374 .and_then(|path| self.breakpoints.get(&path))
375 .into_iter()
376 .flat_map(move |file_breakpoints| {
377 file_breakpoints.breakpoints.iter().filter({
378 let range = range.clone();
379 move |(position, _)| {
380 if let Some(range) = &range {
381 position.cmp(&range.start, buffer_snapshot).is_ge()
382 && position.cmp(&range.end, buffer_snapshot).is_le()
383 } else {
384 true
385 }
386 }
387 })
388 })
389 }
390
391 pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, text::Anchor)> {
392 self.active_stack_frame.as_ref()
393 }
394
395 pub fn remove_active_position(
396 &mut self,
397 session_id: Option<SessionId>,
398 cx: &mut Context<Self>,
399 ) {
400 if let Some(session_id) = session_id {
401 self.active_stack_frame
402 .take_if(|(id, _, _)| *id == session_id);
403 } else {
404 self.active_stack_frame.take();
405 }
406
407 cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
408 cx.notify();
409 }
410
411 pub fn set_active_position(
412 &mut self,
413 position: (SessionId, Arc<Path>, text::Anchor),
414 cx: &mut Context<Self>,
415 ) {
416 self.active_stack_frame = Some(position);
417 cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
418 cx.notify();
419 }
420
421 pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SerializedBreakpoint> {
422 self.breakpoints
423 .get(path)
424 .map(|bp| {
425 let snapshot = bp.buffer.read(cx).snapshot();
426 bp.breakpoints
427 .iter()
428 .map(|(position, breakpoint)| {
429 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
430 SerializedBreakpoint {
431 position,
432 path: path.clone(),
433 kind: breakpoint.kind.clone(),
434 state: breakpoint.state,
435 }
436 })
437 .collect()
438 })
439 .unwrap_or_default()
440 }
441
442 pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> {
443 self.breakpoints
444 .iter()
445 .map(|(path, bp)| {
446 let snapshot = bp.buffer.read(cx).snapshot();
447 (
448 path.clone(),
449 bp.breakpoints
450 .iter()
451 .map(|(position, breakpoint)| {
452 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
453 SerializedBreakpoint {
454 position,
455 path: path.clone(),
456 kind: breakpoint.kind.clone(),
457 state: breakpoint.state,
458 }
459 })
460 .collect(),
461 )
462 })
463 .collect()
464 }
465
466 pub fn with_serialized_breakpoints(
467 &self,
468 breakpoints: BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
469 cx: &mut Context<BreakpointStore>,
470 ) -> Task<Result<()>> {
471 if let BreakpointStoreMode::Local(mode) = &self.mode {
472 let mode = mode.clone();
473 cx.spawn(async move |this, cx| {
474 let mut new_breakpoints = BTreeMap::default();
475 for (path, bps) in breakpoints {
476 if bps.is_empty() {
477 continue;
478 }
479 let (worktree, relative_path) = mode
480 .worktree_store
481 .update(cx, |this, cx| {
482 this.find_or_create_worktree(&path, false, cx)
483 })?
484 .await?;
485 let buffer = mode
486 .buffer_store
487 .update(cx, |this, cx| {
488 let path = ProjectPath {
489 worktree_id: worktree.read(cx).id(),
490 path: relative_path.into(),
491 };
492 this.open_buffer(path, cx)
493 })?
494 .await;
495 let Ok(buffer) = buffer else {
496 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
497 continue;
498 };
499 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
500
501 let mut breakpoints_for_file =
502 this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
503
504 for bp in bps {
505 let position = snapshot.anchor_before(PointUtf16::new(bp.position, 0));
506 breakpoints_for_file.breakpoints.push((
507 position,
508 Breakpoint {
509 kind: bp.kind,
510 state: bp.state,
511 },
512 ))
513 }
514 new_breakpoints.insert(path, breakpoints_for_file);
515 }
516 this.update(cx, |this, cx| {
517 this.breakpoints = new_breakpoints;
518 cx.notify();
519 })?;
520
521 Ok(())
522 })
523 } else {
524 Task::ready(Ok(()))
525 }
526 }
527
528 #[cfg(any(test, feature = "test-support"))]
529 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
530 self.breakpoints.keys().cloned().collect()
531 }
532}
533
534#[derive(Clone, Copy)]
535pub enum BreakpointUpdatedReason {
536 Toggled,
537 FileSaved,
538}
539
540pub enum BreakpointStoreEvent {
541 ActiveDebugLineChanged,
542 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
543 BreakpointsCleared(Vec<Arc<Path>>),
544}
545
546impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
547
548type LogMessage = Arc<str>;
549
550#[derive(Clone, Debug)]
551pub enum BreakpointEditAction {
552 Toggle,
553 InvertState,
554 EditLogMessage(LogMessage),
555}
556
557#[derive(Clone, Debug)]
558pub enum BreakpointKind {
559 Standard,
560 Log(LogMessage),
561}
562
563impl BreakpointKind {
564 pub fn to_int(&self) -> i32 {
565 match self {
566 BreakpointKind::Standard => 0,
567 BreakpointKind::Log(_) => 1,
568 }
569 }
570
571 pub fn log_message(&self) -> Option<LogMessage> {
572 match self {
573 BreakpointKind::Standard => None,
574 BreakpointKind::Log(message) => Some(message.clone()),
575 }
576 }
577}
578
579impl PartialEq for BreakpointKind {
580 fn eq(&self, other: &Self) -> bool {
581 std::mem::discriminant(self) == std::mem::discriminant(other)
582 }
583}
584
585impl Eq for BreakpointKind {}
586
587impl Hash for BreakpointKind {
588 fn hash<H: Hasher>(&self, state: &mut H) {
589 std::mem::discriminant(self).hash(state);
590 }
591}
592
593#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
594pub enum BreakpointState {
595 Enabled,
596 Disabled,
597}
598
599impl BreakpointState {
600 #[inline]
601 pub fn is_enabled(&self) -> bool {
602 matches!(self, BreakpointState::Enabled)
603 }
604
605 #[inline]
606 pub fn is_disabled(&self) -> bool {
607 matches!(self, BreakpointState::Disabled)
608 }
609
610 #[inline]
611 pub fn to_int(&self) -> i32 {
612 match self {
613 BreakpointState::Enabled => 0,
614 BreakpointState::Disabled => 1,
615 }
616 }
617}
618
619#[derive(Clone, Debug, Hash, PartialEq, Eq)]
620pub struct Breakpoint {
621 pub kind: BreakpointKind,
622 pub state: BreakpointState,
623}
624
625impl Breakpoint {
626 pub fn new_standard() -> Self {
627 Self {
628 kind: BreakpointKind::Standard,
629 state: BreakpointState::Enabled,
630 }
631 }
632
633 pub fn new_log(log_message: &str) -> Self {
634 Self {
635 kind: BreakpointKind::Log(log_message.to_owned().into()),
636 state: BreakpointState::Enabled,
637 }
638 }
639
640 fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
641 Some(client::proto::Breakpoint {
642 position: Some(serialize_text_anchor(position)),
643 state: match self.state {
644 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
645 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
646 },
647 kind: match self.kind {
648 BreakpointKind::Standard => proto::BreakpointKind::Standard.into(),
649 BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(),
650 },
651 message: if let BreakpointKind::Log(message) = &self.kind {
652 Some(message.to_string())
653 } else {
654 None
655 },
656 })
657 }
658
659 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
660 Some(Self {
661 kind: match proto::BreakpointKind::from_i32(breakpoint.kind) {
662 Some(proto::BreakpointKind::Log) => {
663 BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into())
664 }
665 None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard,
666 },
667 state: match proto::BreakpointState::from_i32(breakpoint.state) {
668 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
669 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
670 },
671 })
672 }
673
674 #[inline]
675 pub fn is_enabled(&self) -> bool {
676 self.state.is_enabled()
677 }
678
679 #[inline]
680 pub fn is_disabled(&self) -> bool {
681 self.state.is_disabled()
682 }
683}
684
685#[derive(Clone, Debug, Hash, PartialEq, Eq)]
686pub struct SerializedBreakpoint {
687 pub position: u32,
688 pub path: Arc<Path>,
689 pub kind: BreakpointKind,
690 pub state: BreakpointState,
691}
692
693impl From<SerializedBreakpoint> for dap::SourceBreakpoint {
694 fn from(bp: SerializedBreakpoint) -> Self {
695 Self {
696 line: bp.position as u64 + 1,
697 column: None,
698 condition: None,
699 hit_condition: None,
700 log_message: bp.kind.log_message().as_deref().map(Into::into),
701 mode: None,
702 }
703 }
704}