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::{Context as _, Result};
5pub use breakpoints_in_file::{BreakpointSessionState, BreakpointWithPosition};
6use breakpoints_in_file::{BreakpointsInFile, StatefulBreakpoint};
7use collections::{BTreeMap, HashMap};
8use dap::{StackFrameId, client::SessionId};
9use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
10use itertools::Itertools;
11use language::{Buffer, BufferSnapshot, proto::serialize_anchor as serialize_text_anchor};
12use rpc::{
13 AnyProtoClient, TypedEnvelope,
14 proto::{self},
15};
16use std::{hash::Hash, ops::Range, path::Path, sync::Arc, u32};
17use text::{Point, PointUtf16};
18use util::maybe;
19
20use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
21
22use super::session::ThreadId;
23
24mod breakpoints_in_file {
25 use collections::HashMap;
26 use language::{BufferEvent, DiskState};
27
28 use super::*;
29
30 #[derive(Clone, Debug, PartialEq, Eq)]
31 pub struct BreakpointWithPosition {
32 pub position: text::Anchor,
33 pub bp: Breakpoint,
34 }
35
36 /// A breakpoint with per-session data about it's state (as seen by the Debug Adapter).
37 #[derive(Clone, Debug)]
38 pub struct StatefulBreakpoint {
39 pub bp: BreakpointWithPosition,
40 pub session_state: HashMap<SessionId, BreakpointSessionState>,
41 }
42
43 impl StatefulBreakpoint {
44 pub(super) fn new(bp: BreakpointWithPosition) -> Self {
45 Self {
46 bp,
47 session_state: Default::default(),
48 }
49 }
50 pub(super) fn position(&self) -> &text::Anchor {
51 &self.bp.position
52 }
53 }
54
55 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
56 pub struct BreakpointSessionState {
57 /// Session-specific identifier for the breakpoint, as assigned by Debug Adapter.
58 pub id: u64,
59 pub verified: bool,
60 }
61 #[derive(Clone)]
62 pub(super) struct BreakpointsInFile {
63 pub(super) buffer: Entity<Buffer>,
64 // 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
65 pub(super) breakpoints: Vec<StatefulBreakpoint>,
66 _subscription: Arc<Subscription>,
67 }
68
69 impl BreakpointsInFile {
70 pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {
71 let subscription = Arc::from(cx.subscribe(
72 &buffer,
73 |breakpoint_store, buffer, event, cx| match event {
74 BufferEvent::Saved => {
75 if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
76 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
77 abs_path,
78 BreakpointUpdatedReason::FileSaved,
79 ));
80 }
81 }
82 BufferEvent::FileHandleChanged => {
83 let entity_id = buffer.entity_id();
84
85 if buffer.read(cx).file().is_none_or(|f| f.disk_state() == DiskState::Deleted) {
86 breakpoint_store.breakpoints.retain(|_, breakpoints_in_file| {
87 breakpoints_in_file.buffer.entity_id() != entity_id
88 });
89
90 cx.notify();
91 return;
92 }
93
94 if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
95 if breakpoint_store.breakpoints.contains_key(&abs_path) {
96 return;
97 }
98
99 if let Some(old_path) = breakpoint_store
100 .breakpoints
101 .iter()
102 .find(|(_, in_file)| in_file.buffer.entity_id() == entity_id)
103 .map(|values| values.0)
104 .cloned()
105 {
106 let Some(breakpoints_in_file) =
107 breakpoint_store.breakpoints.remove(&old_path) else {
108 log::error!("Couldn't get breakpoints in file from old path during buffer rename handling");
109 return;
110 };
111
112 breakpoint_store.breakpoints.insert(abs_path, breakpoints_in_file);
113 cx.notify();
114 }
115 }
116 }
117 _ => {}
118 },
119 ));
120
121 BreakpointsInFile {
122 buffer,
123 breakpoints: Vec::new(),
124 _subscription: subscription,
125 }
126 }
127 }
128}
129
130#[derive(Clone)]
131struct RemoteBreakpointStore {
132 upstream_client: AnyProtoClient,
133 _upstream_project_id: u64,
134}
135
136#[derive(Clone)]
137struct LocalBreakpointStore {
138 worktree_store: Entity<WorktreeStore>,
139 buffer_store: Entity<BufferStore>,
140}
141
142#[derive(Clone)]
143enum BreakpointStoreMode {
144 Local(LocalBreakpointStore),
145 Remote(RemoteBreakpointStore),
146}
147
148#[derive(Clone, PartialEq)]
149pub struct ActiveStackFrame {
150 pub session_id: SessionId,
151 pub thread_id: ThreadId,
152 pub stack_frame_id: StackFrameId,
153 pub path: Arc<Path>,
154 pub position: text::Anchor,
155}
156
157pub struct BreakpointStore {
158 breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,
159 downstream_client: Option<(AnyProtoClient, u64)>,
160 active_stack_frame: Option<ActiveStackFrame>,
161 // E.g ssh
162 mode: BreakpointStoreMode,
163}
164
165impl BreakpointStore {
166 pub fn init(client: &AnyProtoClient) {
167 client.add_entity_request_handler(Self::handle_toggle_breakpoint);
168 client.add_entity_message_handler(Self::handle_breakpoints_for_file);
169 }
170 pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
171 BreakpointStore {
172 breakpoints: BTreeMap::new(),
173 mode: BreakpointStoreMode::Local(LocalBreakpointStore {
174 worktree_store,
175 buffer_store,
176 }),
177 downstream_client: None,
178 active_stack_frame: Default::default(),
179 }
180 }
181
182 pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
183 BreakpointStore {
184 breakpoints: BTreeMap::new(),
185 mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
186 upstream_client,
187 _upstream_project_id: upstream_project_id,
188 }),
189 downstream_client: None,
190 active_stack_frame: Default::default(),
191 }
192 }
193
194 pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
195 self.downstream_client = Some((downstream_client.clone(), project_id));
196 }
197
198 pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) {
199 self.downstream_client.take();
200
201 cx.notify();
202 }
203
204 async fn handle_breakpoints_for_file(
205 this: Entity<Project>,
206 message: TypedEnvelope<proto::BreakpointsForFile>,
207 mut cx: AsyncApp,
208 ) -> Result<()> {
209 let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
210 if message.payload.breakpoints.is_empty() {
211 return Ok(());
212 }
213
214 let buffer = this
215 .update(&mut cx, |this, cx| {
216 let path =
217 this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
218 Some(this.open_buffer(path, cx))
219 })
220 .ok()
221 .flatten()
222 .context("Invalid project path")?
223 .await?;
224
225 breakpoints.update(&mut cx, move |this, cx| {
226 let bps = this
227 .breakpoints
228 .entry(Arc::<Path>::from(message.payload.path.as_ref()))
229 .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
230
231 bps.breakpoints = message
232 .payload
233 .breakpoints
234 .into_iter()
235 .filter_map(|breakpoint| {
236 let position =
237 language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
238 let session_state = breakpoint
239 .session_state
240 .iter()
241 .map(|(session_id, state)| {
242 let state = BreakpointSessionState {
243 id: state.id,
244 verified: state.verified,
245 };
246 (SessionId::from_proto(*session_id), state)
247 })
248 .collect();
249 let breakpoint = Breakpoint::from_proto(breakpoint)?;
250 let bp = BreakpointWithPosition {
251 position,
252 bp: breakpoint,
253 };
254
255 Some(StatefulBreakpoint { bp, session_state })
256 })
257 .collect();
258
259 cx.notify();
260 })?;
261
262 Ok(())
263 }
264
265 async fn handle_toggle_breakpoint(
266 this: Entity<Project>,
267 message: TypedEnvelope<proto::ToggleBreakpoint>,
268 mut cx: AsyncApp,
269 ) -> Result<proto::Ack> {
270 let breakpoints = this.read_with(&mut cx, |this, _| this.breakpoint_store())?;
271 let path = this
272 .update(&mut cx, |this, cx| {
273 this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)
274 })?
275 .context("Could not resolve provided abs path")?;
276 let buffer = this
277 .update(&mut cx, |this, cx| {
278 this.buffer_store().read(cx).get_by_path(&path)
279 })?
280 .context("Could not find buffer for a given path")?;
281 let breakpoint = message
282 .payload
283 .breakpoint
284 .context("Breakpoint not present in RPC payload")?;
285 let position = language::proto::deserialize_anchor(
286 breakpoint
287 .position
288 .clone()
289 .context("Anchor not present in RPC payload")?,
290 )
291 .context("Anchor deserialization failed")?;
292 let breakpoint =
293 Breakpoint::from_proto(breakpoint).context("Could not deserialize breakpoint")?;
294
295 breakpoints.update(&mut cx, |this, cx| {
296 this.toggle_breakpoint(
297 buffer,
298 BreakpointWithPosition {
299 position,
300 bp: breakpoint,
301 },
302 BreakpointEditAction::Toggle,
303 cx,
304 );
305 })?;
306 Ok(proto::Ack {})
307 }
308
309 pub(crate) fn broadcast(&self) {
310 if let Some((client, project_id)) = &self.downstream_client {
311 for (path, breakpoint_set) in &self.breakpoints {
312 let _ = client.send(proto::BreakpointsForFile {
313 project_id: *project_id,
314 path: path.to_str().map(ToOwned::to_owned).unwrap(),
315 breakpoints: breakpoint_set
316 .breakpoints
317 .iter()
318 .filter_map(|breakpoint| {
319 breakpoint.bp.bp.to_proto(
320 &path,
321 &breakpoint.position(),
322 &breakpoint.session_state,
323 )
324 })
325 .collect(),
326 });
327 }
328 }
329 }
330
331 pub(crate) fn update_session_breakpoint(
332 &mut self,
333 session_id: SessionId,
334 _: dap::BreakpointEventReason,
335 breakpoint: dap::Breakpoint,
336 ) {
337 maybe!({
338 let event_id = breakpoint.id?;
339
340 let state = self
341 .breakpoints
342 .values_mut()
343 .find_map(|breakpoints_in_file| {
344 breakpoints_in_file
345 .breakpoints
346 .iter_mut()
347 .find_map(|state| {
348 let state = state.session_state.get_mut(&session_id)?;
349
350 if state.id == event_id {
351 Some(state)
352 } else {
353 None
354 }
355 })
356 })?;
357
358 state.verified = breakpoint.verified;
359 Some(())
360 });
361 }
362
363 pub(super) fn mark_breakpoints_verified(
364 &mut self,
365 session_id: SessionId,
366 abs_path: &Path,
367
368 it: impl Iterator<Item = (BreakpointWithPosition, BreakpointSessionState)>,
369 ) {
370 maybe!({
371 let breakpoints = self.breakpoints.get_mut(abs_path)?;
372 for (breakpoint, state) in it {
373 if let Some(to_update) = breakpoints
374 .breakpoints
375 .iter_mut()
376 .find(|bp| *bp.position() == breakpoint.position)
377 {
378 to_update
379 .session_state
380 .entry(session_id)
381 .insert_entry(state);
382 }
383 }
384 Some(())
385 });
386 }
387
388 pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
389 worktree::File::from_dyn(buffer.read(cx).file())
390 .and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
391 .map(Arc::<Path>::from)
392 }
393
394 pub fn toggle_breakpoint(
395 &mut self,
396 buffer: Entity<Buffer>,
397 mut breakpoint: BreakpointWithPosition,
398 edit_action: BreakpointEditAction,
399 cx: &mut Context<Self>,
400 ) {
401 let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
402 return;
403 };
404
405 let breakpoint_set = self
406 .breakpoints
407 .entry(abs_path.clone())
408 .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
409
410 match edit_action {
411 BreakpointEditAction::Toggle => {
412 let len_before = breakpoint_set.breakpoints.len();
413 breakpoint_set
414 .breakpoints
415 .retain(|value| breakpoint != value.bp);
416 if len_before == breakpoint_set.breakpoints.len() {
417 // We did not remove any breakpoint, hence let's toggle one.
418 breakpoint_set
419 .breakpoints
420 .push(StatefulBreakpoint::new(breakpoint.clone()));
421 }
422 }
423 BreakpointEditAction::InvertState => {
424 if let Some(bp) = breakpoint_set
425 .breakpoints
426 .iter_mut()
427 .find(|value| breakpoint == value.bp)
428 {
429 let bp = &mut bp.bp.bp;
430 if bp.is_enabled() {
431 bp.state = BreakpointState::Disabled;
432 } else {
433 bp.state = BreakpointState::Enabled;
434 }
435 } else {
436 breakpoint.bp.state = BreakpointState::Disabled;
437 breakpoint_set
438 .breakpoints
439 .push(StatefulBreakpoint::new(breakpoint.clone()));
440 }
441 }
442 BreakpointEditAction::EditLogMessage(log_message) => {
443 if !log_message.is_empty() {
444 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| {
445 if breakpoint.position == *bp.position() {
446 Some(&mut bp.bp.bp)
447 } else {
448 None
449 }
450 });
451
452 if let Some(found_bp) = found_bp {
453 found_bp.message = Some(log_message.clone());
454 } else {
455 breakpoint.bp.message = Some(log_message.clone());
456 // We did not remove any breakpoint, hence let's toggle one.
457 breakpoint_set
458 .breakpoints
459 .push(StatefulBreakpoint::new(breakpoint.clone()));
460 }
461 } else if breakpoint.bp.message.is_some() {
462 if let Some(position) = breakpoint_set
463 .breakpoints
464 .iter()
465 .find_position(|other| breakpoint == other.bp)
466 .map(|res| res.0)
467 {
468 breakpoint_set.breakpoints.remove(position);
469 } else {
470 log::error!("Failed to find position of breakpoint to delete")
471 }
472 }
473 }
474 BreakpointEditAction::EditHitCondition(hit_condition) => {
475 if !hit_condition.is_empty() {
476 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
477 if breakpoint.position == *other.position() {
478 Some(&mut other.bp.bp)
479 } else {
480 None
481 }
482 });
483
484 if let Some(found_bp) = found_bp {
485 found_bp.hit_condition = Some(hit_condition.clone());
486 } else {
487 breakpoint.bp.hit_condition = Some(hit_condition.clone());
488 // We did not remove any breakpoint, hence let's toggle one.
489 breakpoint_set
490 .breakpoints
491 .push(StatefulBreakpoint::new(breakpoint.clone()))
492 }
493 } else if breakpoint.bp.hit_condition.is_some() {
494 if let Some(position) = breakpoint_set
495 .breakpoints
496 .iter()
497 .find_position(|bp| breakpoint == bp.bp)
498 .map(|res| res.0)
499 {
500 breakpoint_set.breakpoints.remove(position);
501 } else {
502 log::error!("Failed to find position of breakpoint to delete")
503 }
504 }
505 }
506 BreakpointEditAction::EditCondition(condition) => {
507 if !condition.is_empty() {
508 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
509 if breakpoint.position == *other.position() {
510 Some(&mut other.bp.bp)
511 } else {
512 None
513 }
514 });
515
516 if let Some(found_bp) = found_bp {
517 found_bp.condition = Some(condition.clone());
518 } else {
519 breakpoint.bp.condition = Some(condition.clone());
520 // We did not remove any breakpoint, hence let's toggle one.
521 breakpoint_set
522 .breakpoints
523 .push(StatefulBreakpoint::new(breakpoint.clone()));
524 }
525 } else if breakpoint.bp.condition.is_some() {
526 if let Some(position) = breakpoint_set
527 .breakpoints
528 .iter()
529 .find_position(|bp| breakpoint == bp.bp)
530 .map(|res| res.0)
531 {
532 breakpoint_set.breakpoints.remove(position);
533 } else {
534 log::error!("Failed to find position of breakpoint to delete")
535 }
536 }
537 }
538 }
539
540 if breakpoint_set.breakpoints.is_empty() {
541 self.breakpoints.remove(&abs_path);
542 }
543 if let BreakpointStoreMode::Remote(remote) = &self.mode {
544 if let Some(breakpoint) =
545 breakpoint
546 .bp
547 .to_proto(&abs_path, &breakpoint.position, &HashMap::default())
548 {
549 cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
550 project_id: remote._upstream_project_id,
551 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
552 breakpoint: Some(breakpoint),
553 }))
554 .detach();
555 }
556 } else if let Some((client, project_id)) = &self.downstream_client {
557 let breakpoints = self
558 .breakpoints
559 .get(&abs_path)
560 .map(|breakpoint_set| {
561 breakpoint_set
562 .breakpoints
563 .iter()
564 .filter_map(|bp| {
565 bp.bp
566 .bp
567 .to_proto(&abs_path, bp.position(), &bp.session_state)
568 })
569 .collect()
570 })
571 .unwrap_or_default();
572
573 let _ = client.send(proto::BreakpointsForFile {
574 project_id: *project_id,
575 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
576 breakpoints,
577 });
578 }
579
580 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
581 abs_path,
582 BreakpointUpdatedReason::Toggled,
583 ));
584 cx.notify();
585 }
586
587 pub fn on_file_rename(
588 &mut self,
589 old_path: Arc<Path>,
590 new_path: Arc<Path>,
591 cx: &mut Context<Self>,
592 ) {
593 if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
594 self.breakpoints.insert(new_path.clone(), breakpoints);
595
596 cx.notify();
597 }
598 }
599
600 pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
601 let breakpoint_paths = self.breakpoints.keys().cloned().collect();
602 self.breakpoints.clear();
603 cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
604 }
605
606 pub fn breakpoints<'a>(
607 &'a self,
608 buffer: &'a Entity<Buffer>,
609 range: Option<Range<text::Anchor>>,
610 buffer_snapshot: &'a BufferSnapshot,
611 cx: &App,
612 ) -> impl Iterator<Item = (&'a BreakpointWithPosition, Option<BreakpointSessionState>)> + 'a
613 {
614 let abs_path = Self::abs_path_from_buffer(buffer, cx);
615 let active_session_id = self
616 .active_stack_frame
617 .as_ref()
618 .map(|frame| frame.session_id);
619 abs_path
620 .and_then(|path| self.breakpoints.get(&path))
621 .into_iter()
622 .flat_map(move |file_breakpoints| {
623 file_breakpoints.breakpoints.iter().filter_map({
624 let range = range.clone();
625 move |bp| {
626 if let Some(range) = &range {
627 if bp.position().cmp(&range.start, buffer_snapshot).is_lt()
628 || bp.position().cmp(&range.end, buffer_snapshot).is_gt()
629 {
630 return None;
631 }
632 }
633 let session_state = active_session_id
634 .and_then(|id| bp.session_state.get(&id))
635 .copied();
636 Some((&bp.bp, session_state))
637 }
638 })
639 })
640 }
641
642 pub fn active_position(&self) -> Option<&ActiveStackFrame> {
643 self.active_stack_frame.as_ref()
644 }
645
646 pub fn remove_active_position(
647 &mut self,
648 session_id: Option<SessionId>,
649 cx: &mut Context<Self>,
650 ) {
651 if let Some(session_id) = session_id {
652 self.active_stack_frame
653 .take_if(|active_stack_frame| active_stack_frame.session_id == session_id);
654 } else {
655 self.active_stack_frame.take();
656 }
657
658 cx.emit(BreakpointStoreEvent::ClearDebugLines);
659 cx.notify();
660 }
661
662 pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context<Self>) {
663 if self
664 .active_stack_frame
665 .as_ref()
666 .is_some_and(|active_position| active_position == &position)
667 {
668 cx.emit(BreakpointStoreEvent::SetDebugLine);
669 return;
670 }
671
672 if self.active_stack_frame.is_some() {
673 cx.emit(BreakpointStoreEvent::ClearDebugLines);
674 }
675
676 self.active_stack_frame = Some(position);
677
678 cx.emit(BreakpointStoreEvent::SetDebugLine);
679 cx.notify();
680 }
681
682 pub fn breakpoint_at_row(
683 &self,
684 path: &Path,
685 row: u32,
686 cx: &App,
687 ) -> Option<(Entity<Buffer>, BreakpointWithPosition)> {
688 self.breakpoints.get(path).and_then(|breakpoints| {
689 let snapshot = breakpoints.buffer.read(cx).text_snapshot();
690
691 breakpoints
692 .breakpoints
693 .iter()
694 .find(|bp| bp.position().summary::<Point>(&snapshot).row == row)
695 .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone()))
696 })
697 }
698
699 pub fn breakpoints_from_path(&self, path: &Arc<Path>) -> Vec<BreakpointWithPosition> {
700 self.breakpoints
701 .get(path)
702 .map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect())
703 .unwrap_or_default()
704 }
705
706 pub fn source_breakpoints_from_path(
707 &self,
708 path: &Arc<Path>,
709 cx: &App,
710 ) -> Vec<SourceBreakpoint> {
711 self.breakpoints
712 .get(path)
713 .map(|bp| {
714 let snapshot = bp.buffer.read(cx).snapshot();
715 bp.breakpoints
716 .iter()
717 .map(|bp| {
718 let position = snapshot.summary_for_anchor::<PointUtf16>(bp.position()).row;
719 let bp = &bp.bp;
720 SourceBreakpoint {
721 row: position,
722 path: path.clone(),
723 state: bp.bp.state,
724 message: bp.bp.message.clone(),
725 condition: bp.bp.condition.clone(),
726 hit_condition: bp.bp.hit_condition.clone(),
727 }
728 })
729 .collect()
730 })
731 .unwrap_or_default()
732 }
733
734 pub fn all_breakpoints(&self) -> BTreeMap<Arc<Path>, Vec<BreakpointWithPosition>> {
735 self.breakpoints
736 .iter()
737 .map(|(path, bp)| {
738 (
739 path.clone(),
740 bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(),
741 )
742 })
743 .collect()
744 }
745 pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
746 self.breakpoints
747 .iter()
748 .map(|(path, bp)| {
749 let snapshot = bp.buffer.read(cx).snapshot();
750 (
751 path.clone(),
752 bp.breakpoints
753 .iter()
754 .map(|breakpoint| {
755 let position = snapshot
756 .summary_for_anchor::<PointUtf16>(&breakpoint.position())
757 .row;
758 let breakpoint = &breakpoint.bp;
759 SourceBreakpoint {
760 row: position,
761 path: path.clone(),
762 message: breakpoint.bp.message.clone(),
763 state: breakpoint.bp.state,
764 hit_condition: breakpoint.bp.hit_condition.clone(),
765 condition: breakpoint.bp.condition.clone(),
766 }
767 })
768 .collect(),
769 )
770 })
771 .collect()
772 }
773
774 pub fn with_serialized_breakpoints(
775 &self,
776 breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
777 cx: &mut Context<BreakpointStore>,
778 ) -> Task<Result<()>> {
779 if let BreakpointStoreMode::Local(mode) = &self.mode {
780 let mode = mode.clone();
781 cx.spawn(async move |this, cx| {
782 let mut new_breakpoints = BTreeMap::default();
783 for (path, bps) in breakpoints {
784 if bps.is_empty() {
785 continue;
786 }
787 let (worktree, relative_path) = mode
788 .worktree_store
789 .update(cx, |this, cx| {
790 this.find_or_create_worktree(&path, false, cx)
791 })?
792 .await?;
793 let buffer = mode
794 .buffer_store
795 .update(cx, |this, cx| {
796 let path = ProjectPath {
797 worktree_id: worktree.read(cx).id(),
798 path: relative_path.into(),
799 };
800 this.open_buffer(path, cx)
801 })?
802 .await;
803 let Ok(buffer) = buffer else {
804 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
805 continue;
806 };
807 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
808
809 let mut breakpoints_for_file =
810 this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
811
812 for bp in bps {
813 let max_point = snapshot.max_point_utf16();
814 let point = PointUtf16::new(bp.row, 0);
815 if point > max_point {
816 log::error!("skipping a deserialized breakpoint that's out of range");
817 continue;
818 }
819 let position = snapshot.anchor_after(point);
820 breakpoints_for_file
821 .breakpoints
822 .push(StatefulBreakpoint::new(BreakpointWithPosition {
823 position,
824 bp: Breakpoint {
825 message: bp.message,
826 state: bp.state,
827 condition: bp.condition,
828 hit_condition: bp.hit_condition,
829 },
830 }))
831 }
832 new_breakpoints.insert(path, breakpoints_for_file);
833 }
834 this.update(cx, |this, cx| {
835 log::info!("Finish deserializing breakpoints & initializing breakpoint store");
836 for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
837 (path.to_string_lossy(), bp_in_file.breakpoints.len())
838 }) {
839 let breakpoint_str = if count > 1 {
840 "breakpoints"
841 } else {
842 "breakpoint"
843 };
844 log::debug!("Deserialized {count} {breakpoint_str} at path: {path}");
845 }
846
847 this.breakpoints = new_breakpoints;
848
849 cx.notify();
850 })?;
851
852 Ok(())
853 })
854 } else {
855 Task::ready(Ok(()))
856 }
857 }
858
859 #[cfg(any(test, feature = "test-support"))]
860 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
861 self.breakpoints.keys().cloned().collect()
862 }
863}
864
865#[derive(Clone, Copy)]
866pub enum BreakpointUpdatedReason {
867 Toggled,
868 FileSaved,
869}
870
871pub enum BreakpointStoreEvent {
872 SetDebugLine,
873 ClearDebugLines,
874 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
875 BreakpointsCleared(Vec<Arc<Path>>),
876}
877
878impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
879
880type BreakpointMessage = Arc<str>;
881
882#[derive(Clone, Debug)]
883pub enum BreakpointEditAction {
884 Toggle,
885 InvertState,
886 EditLogMessage(BreakpointMessage),
887 EditCondition(BreakpointMessage),
888 EditHitCondition(BreakpointMessage),
889}
890
891#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
892pub enum BreakpointState {
893 Enabled,
894 Disabled,
895}
896
897impl BreakpointState {
898 #[inline]
899 pub fn is_enabled(&self) -> bool {
900 matches!(self, BreakpointState::Enabled)
901 }
902
903 #[inline]
904 pub fn is_disabled(&self) -> bool {
905 matches!(self, BreakpointState::Disabled)
906 }
907
908 #[inline]
909 pub fn to_int(&self) -> i32 {
910 match self {
911 BreakpointState::Enabled => 0,
912 BreakpointState::Disabled => 1,
913 }
914 }
915}
916
917#[derive(Clone, Debug, Hash, PartialEq, Eq)]
918pub struct Breakpoint {
919 pub message: Option<BreakpointMessage>,
920 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
921 pub hit_condition: Option<Arc<str>>,
922 pub condition: Option<BreakpointMessage>,
923 pub state: BreakpointState,
924}
925
926impl Breakpoint {
927 pub fn new_standard() -> Self {
928 Self {
929 state: BreakpointState::Enabled,
930 hit_condition: None,
931 condition: None,
932 message: None,
933 }
934 }
935
936 pub fn new_condition(hit_condition: &str) -> Self {
937 Self {
938 state: BreakpointState::Enabled,
939 condition: None,
940 hit_condition: Some(hit_condition.into()),
941 message: None,
942 }
943 }
944
945 pub fn new_log(log_message: &str) -> Self {
946 Self {
947 state: BreakpointState::Enabled,
948 hit_condition: None,
949 condition: None,
950 message: Some(log_message.into()),
951 }
952 }
953
954 fn to_proto(
955 &self,
956 _path: &Path,
957 position: &text::Anchor,
958 session_states: &HashMap<SessionId, BreakpointSessionState>,
959 ) -> Option<client::proto::Breakpoint> {
960 Some(client::proto::Breakpoint {
961 position: Some(serialize_text_anchor(position)),
962 state: match self.state {
963 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
964 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
965 },
966 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
967 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
968 hit_condition: self
969 .hit_condition
970 .as_ref()
971 .map(|s| String::from(s.as_ref())),
972 session_state: session_states
973 .iter()
974 .map(|(session_id, state)| {
975 (
976 session_id.to_proto(),
977 proto::BreakpointSessionState {
978 id: state.id,
979 verified: state.verified,
980 },
981 )
982 })
983 .collect(),
984 })
985 }
986
987 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
988 Some(Self {
989 state: match proto::BreakpointState::from_i32(breakpoint.state) {
990 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
991 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
992 },
993 message: breakpoint.message.map(Into::into),
994 condition: breakpoint.condition.map(Into::into),
995 hit_condition: breakpoint.hit_condition.map(Into::into),
996 })
997 }
998
999 #[inline]
1000 pub fn is_enabled(&self) -> bool {
1001 self.state.is_enabled()
1002 }
1003
1004 #[inline]
1005 pub fn is_disabled(&self) -> bool {
1006 self.state.is_disabled()
1007 }
1008}
1009
1010/// Breakpoint for location within source code.
1011#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1012pub struct SourceBreakpoint {
1013 pub row: u32,
1014 pub path: Arc<Path>,
1015 pub message: Option<Arc<str>>,
1016 pub condition: Option<Arc<str>>,
1017 pub hit_condition: Option<Arc<str>>,
1018 pub state: BreakpointState,
1019}
1020
1021impl From<SourceBreakpoint> for dap::SourceBreakpoint {
1022 fn from(bp: SourceBreakpoint) -> Self {
1023 Self {
1024 line: bp.row as u64 + 1,
1025 column: None,
1026 condition: bp
1027 .condition
1028 .map(|condition| String::from(condition.as_ref())),
1029 hit_condition: bp
1030 .hit_condition
1031 .map(|hit_condition| String::from(hit_condition.as_ref())),
1032 log_message: bp.message.map(|message| String::from(message.as_ref())),
1033 mode: None,
1034 }
1035 }
1036}