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