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, 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(&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 .map(|file| file.worktree.read(cx).absolutize(&file.path))
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);
454 } else {
455 breakpoint.bp.message = Some(log_message);
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);
486 } else {
487 breakpoint.bp.hit_condition = Some(hit_condition);
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);
518 } else {
519 breakpoint.bp.condition = Some(condition);
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, 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 && (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 let session_state = active_session_id
633 .and_then(|id| bp.session_state.get(&id))
634 .copied();
635 Some((&bp.bp, session_state))
636 }
637 })
638 })
639 }
640
641 pub fn active_position(&self) -> Option<&ActiveStackFrame> {
642 self.active_stack_frame.as_ref()
643 }
644
645 pub fn remove_active_position(
646 &mut self,
647 session_id: Option<SessionId>,
648 cx: &mut Context<Self>,
649 ) {
650 if let Some(session_id) = session_id {
651 self.active_stack_frame
652 .take_if(|active_stack_frame| active_stack_frame.session_id == session_id);
653 } else {
654 self.active_stack_frame.take();
655 }
656
657 cx.emit(BreakpointStoreEvent::ClearDebugLines);
658 cx.notify();
659 }
660
661 pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context<Self>) {
662 if self
663 .active_stack_frame
664 .as_ref()
665 .is_some_and(|active_position| active_position == &position)
666 {
667 cx.emit(BreakpointStoreEvent::SetDebugLine);
668 return;
669 }
670
671 if self.active_stack_frame.is_some() {
672 cx.emit(BreakpointStoreEvent::ClearDebugLines);
673 }
674
675 self.active_stack_frame = Some(position);
676
677 cx.emit(BreakpointStoreEvent::SetDebugLine);
678 cx.notify();
679 }
680
681 pub fn breakpoint_at_row(
682 &self,
683 path: &Path,
684 row: u32,
685 cx: &App,
686 ) -> Option<(Entity<Buffer>, BreakpointWithPosition)> {
687 self.breakpoints.get(path).and_then(|breakpoints| {
688 let snapshot = breakpoints.buffer.read(cx).text_snapshot();
689
690 breakpoints
691 .breakpoints
692 .iter()
693 .find(|bp| bp.position().summary::<Point>(&snapshot).row == row)
694 .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone()))
695 })
696 }
697
698 pub fn breakpoints_from_path(&self, path: &Arc<Path>) -> Vec<BreakpointWithPosition> {
699 self.breakpoints
700 .get(path)
701 .map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect())
702 .unwrap_or_default()
703 }
704
705 pub fn source_breakpoints_from_path(
706 &self,
707 path: &Arc<Path>,
708 cx: &App,
709 ) -> Vec<SourceBreakpoint> {
710 self.breakpoints
711 .get(path)
712 .map(|bp| {
713 let snapshot = bp.buffer.read(cx).snapshot();
714 bp.breakpoints
715 .iter()
716 .map(|bp| {
717 let position = snapshot.summary_for_anchor::<PointUtf16>(bp.position()).row;
718 let bp = &bp.bp;
719 SourceBreakpoint {
720 row: position,
721 path: path.clone(),
722 state: bp.bp.state,
723 message: bp.bp.message.clone(),
724 condition: bp.bp.condition.clone(),
725 hit_condition: bp.bp.hit_condition.clone(),
726 }
727 })
728 .collect()
729 })
730 .unwrap_or_default()
731 }
732
733 pub fn all_breakpoints(&self) -> BTreeMap<Arc<Path>, Vec<BreakpointWithPosition>> {
734 self.breakpoints
735 .iter()
736 .map(|(path, bp)| {
737 (
738 path.clone(),
739 bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(),
740 )
741 })
742 .collect()
743 }
744 pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
745 self.breakpoints
746 .iter()
747 .map(|(path, bp)| {
748 let snapshot = bp.buffer.read(cx).snapshot();
749 (
750 path.clone(),
751 bp.breakpoints
752 .iter()
753 .map(|breakpoint| {
754 let position = snapshot
755 .summary_for_anchor::<PointUtf16>(breakpoint.position())
756 .row;
757 let breakpoint = &breakpoint.bp;
758 SourceBreakpoint {
759 row: position,
760 path: path.clone(),
761 message: breakpoint.bp.message.clone(),
762 state: breakpoint.bp.state,
763 hit_condition: breakpoint.bp.hit_condition.clone(),
764 condition: breakpoint.bp.condition.clone(),
765 }
766 })
767 .collect(),
768 )
769 })
770 .collect()
771 }
772
773 pub fn with_serialized_breakpoints(
774 &self,
775 breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
776 cx: &mut Context<BreakpointStore>,
777 ) -> Task<Result<()>> {
778 if let BreakpointStoreMode::Local(mode) = &self.mode {
779 let mode = mode.clone();
780 cx.spawn(async move |this, cx| {
781 let mut new_breakpoints = BTreeMap::default();
782 for (path, bps) in breakpoints {
783 if bps.is_empty() {
784 continue;
785 }
786 let (worktree, relative_path) = mode
787 .worktree_store
788 .update(cx, |this, cx| {
789 this.find_or_create_worktree(&path, false, cx)
790 })?
791 .await?;
792 let buffer = mode
793 .buffer_store
794 .update(cx, |this, cx| {
795 let path = ProjectPath {
796 worktree_id: worktree.read(cx).id(),
797 path: relative_path,
798 };
799 this.open_buffer(path, cx)
800 })?
801 .await;
802 let Ok(buffer) = buffer else {
803 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
804 continue;
805 };
806 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
807
808 let mut breakpoints_for_file =
809 this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
810
811 for bp in bps {
812 let max_point = snapshot.max_point_utf16();
813 let point = PointUtf16::new(bp.row, 0);
814 if point > max_point {
815 log::error!("skipping a deserialized breakpoint that's out of range");
816 continue;
817 }
818 let position = snapshot.anchor_after(point);
819 breakpoints_for_file
820 .breakpoints
821 .push(StatefulBreakpoint::new(BreakpointWithPosition {
822 position,
823 bp: Breakpoint {
824 message: bp.message,
825 state: bp.state,
826 condition: bp.condition,
827 hit_condition: bp.hit_condition,
828 },
829 }))
830 }
831 new_breakpoints.insert(path, breakpoints_for_file);
832 }
833 this.update(cx, |this, cx| {
834 for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
835 (path.to_string_lossy(), bp_in_file.breakpoints.len())
836 }) {
837 let breakpoint_str = if count > 1 {
838 "breakpoints"
839 } else {
840 "breakpoint"
841 };
842 log::debug!("Deserialized {count} {breakpoint_str} at path: {path}");
843 }
844
845 this.breakpoints = new_breakpoints;
846
847 cx.notify();
848 })?;
849
850 Ok(())
851 })
852 } else {
853 Task::ready(Ok(()))
854 }
855 }
856
857 #[cfg(any(test, feature = "test-support"))]
858 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
859 self.breakpoints.keys().cloned().collect()
860 }
861}
862
863#[derive(Clone, Copy)]
864pub enum BreakpointUpdatedReason {
865 Toggled,
866 FileSaved,
867}
868
869pub enum BreakpointStoreEvent {
870 SetDebugLine,
871 ClearDebugLines,
872 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
873 BreakpointsCleared(Vec<Arc<Path>>),
874}
875
876impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
877
878type BreakpointMessage = Arc<str>;
879
880#[derive(Clone, Debug)]
881pub enum BreakpointEditAction {
882 Toggle,
883 InvertState,
884 EditLogMessage(BreakpointMessage),
885 EditCondition(BreakpointMessage),
886 EditHitCondition(BreakpointMessage),
887}
888
889#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
890pub enum BreakpointState {
891 Enabled,
892 Disabled,
893}
894
895impl BreakpointState {
896 #[inline]
897 pub fn is_enabled(&self) -> bool {
898 matches!(self, BreakpointState::Enabled)
899 }
900
901 #[inline]
902 pub fn is_disabled(&self) -> bool {
903 matches!(self, BreakpointState::Disabled)
904 }
905
906 #[inline]
907 pub fn to_int(self) -> i32 {
908 match self {
909 BreakpointState::Enabled => 0,
910 BreakpointState::Disabled => 1,
911 }
912 }
913}
914
915#[derive(Clone, Debug, Hash, PartialEq, Eq)]
916pub struct Breakpoint {
917 pub message: Option<BreakpointMessage>,
918 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
919 pub hit_condition: Option<Arc<str>>,
920 pub condition: Option<BreakpointMessage>,
921 pub state: BreakpointState,
922}
923
924impl Breakpoint {
925 pub fn new_standard() -> Self {
926 Self {
927 state: BreakpointState::Enabled,
928 hit_condition: None,
929 condition: None,
930 message: None,
931 }
932 }
933
934 pub fn new_condition(hit_condition: &str) -> Self {
935 Self {
936 state: BreakpointState::Enabled,
937 condition: None,
938 hit_condition: Some(hit_condition.into()),
939 message: None,
940 }
941 }
942
943 pub fn new_log(log_message: &str) -> Self {
944 Self {
945 state: BreakpointState::Enabled,
946 hit_condition: None,
947 condition: None,
948 message: Some(log_message.into()),
949 }
950 }
951
952 fn to_proto(
953 &self,
954 _path: &Path,
955 position: &text::Anchor,
956 session_states: &HashMap<SessionId, BreakpointSessionState>,
957 ) -> Option<client::proto::Breakpoint> {
958 Some(client::proto::Breakpoint {
959 position: Some(serialize_text_anchor(position)),
960 state: match self.state {
961 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
962 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
963 },
964 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
965 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
966 hit_condition: self
967 .hit_condition
968 .as_ref()
969 .map(|s| String::from(s.as_ref())),
970 session_state: session_states
971 .iter()
972 .map(|(session_id, state)| {
973 (
974 session_id.to_proto(),
975 proto::BreakpointSessionState {
976 id: state.id,
977 verified: state.verified,
978 },
979 )
980 })
981 .collect(),
982 })
983 }
984
985 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
986 Some(Self {
987 state: match proto::BreakpointState::from_i32(breakpoint.state) {
988 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
989 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
990 },
991 message: breakpoint.message.map(Into::into),
992 condition: breakpoint.condition.map(Into::into),
993 hit_condition: breakpoint.hit_condition.map(Into::into),
994 })
995 }
996
997 #[inline]
998 pub fn is_enabled(&self) -> bool {
999 self.state.is_enabled()
1000 }
1001
1002 #[inline]
1003 pub fn is_disabled(&self) -> bool {
1004 self.state.is_disabled()
1005 }
1006}
1007
1008/// Breakpoint for location within source code.
1009#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1010pub struct SourceBreakpoint {
1011 pub row: u32,
1012 pub path: Arc<Path>,
1013 pub message: Option<Arc<str>>,
1014 pub condition: Option<Arc<str>>,
1015 pub hit_condition: Option<Arc<str>>,
1016 pub state: BreakpointState,
1017}
1018
1019impl From<SourceBreakpoint> for dap::SourceBreakpoint {
1020 fn from(bp: SourceBreakpoint) -> Self {
1021 Self {
1022 line: bp.row as u64 + 1,
1023 column: None,
1024 condition: bp
1025 .condition
1026 .map(|condition| String::from(condition.as_ref())),
1027 hit_condition: bp
1028 .hit_condition
1029 .map(|hit_condition| String::from(hit_condition.as_ref())),
1030 log_message: bp.message.map(|message| String::from(message.as_ref())),
1031 mode: None,
1032 }
1033 }
1034}