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