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 log::error!("breakpoint store init");
168 client.add_entity_request_handler(Self::handle_toggle_breakpoint);
169 client.add_entity_message_handler(Self::handle_breakpoints_for_file);
170 }
171 pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
172 BreakpointStore {
173 breakpoints: BTreeMap::new(),
174 mode: BreakpointStoreMode::Local(LocalBreakpointStore {
175 worktree_store,
176 buffer_store,
177 }),
178 downstream_client: None,
179 active_stack_frame: Default::default(),
180 }
181 }
182
183 pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
184 BreakpointStore {
185 breakpoints: BTreeMap::new(),
186 mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
187 upstream_client,
188 _upstream_project_id: upstream_project_id,
189 }),
190 downstream_client: None,
191 active_stack_frame: Default::default(),
192 }
193 }
194
195 pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
196 self.downstream_client = Some((downstream_client, project_id));
197 }
198
199 pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) {
200 self.downstream_client.take();
201
202 cx.notify();
203 }
204
205 async fn handle_breakpoints_for_file(
206 this: Entity<Project>,
207 message: TypedEnvelope<proto::BreakpointsForFile>,
208 mut cx: AsyncApp,
209 ) -> Result<()> {
210 let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
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 =
218 this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
219 Some(this.open_buffer(path, cx))
220 })
221 .ok()
222 .flatten()
223 .context("Invalid project path")?
224 .await?;
225
226 breakpoints.update(&mut cx, move |this, cx| {
227 let bps = this
228 .breakpoints
229 .entry(Arc::<Path>::from(message.payload.path.as_ref()))
230 .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
231
232 bps.breakpoints = message
233 .payload
234 .breakpoints
235 .into_iter()
236 .filter_map(|breakpoint| {
237 let position =
238 language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
239 let session_state = breakpoint
240 .session_state
241 .iter()
242 .map(|(session_id, state)| {
243 let state = BreakpointSessionState {
244 id: state.id,
245 verified: state.verified,
246 };
247 (SessionId::from_proto(*session_id), state)
248 })
249 .collect();
250 let breakpoint = Breakpoint::from_proto(breakpoint)?;
251 let bp = BreakpointWithPosition {
252 position,
253 bp: breakpoint,
254 };
255
256 Some(StatefulBreakpoint { bp, session_state })
257 })
258 .collect();
259
260 cx.notify();
261 })?;
262
263 Ok(())
264 }
265
266 async fn handle_toggle_breakpoint(
267 this: Entity<Project>,
268 message: TypedEnvelope<proto::ToggleBreakpoint>,
269 mut cx: AsyncApp,
270 ) -> Result<proto::Ack> {
271 let breakpoints = this.read_with(&cx, |this, _| this.breakpoint_store())?;
272 let path = this
273 .update(&mut cx, |this, cx| {
274 this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)
275 })?
276 .context("Could not resolve provided abs path")?;
277 let buffer = this
278 .update(&mut cx, |this, cx| {
279 this.buffer_store().read(cx).get_by_path(&path)
280 })?
281 .context("Could not find buffer for a given path")?;
282 let breakpoint = message
283 .payload
284 .breakpoint
285 .context("Breakpoint not present in RPC payload")?;
286 let position = language::proto::deserialize_anchor(
287 breakpoint
288 .position
289 .clone()
290 .context("Anchor not present in RPC payload")?,
291 )
292 .context("Anchor deserialization failed")?;
293 let breakpoint =
294 Breakpoint::from_proto(breakpoint).context("Could not deserialize breakpoint")?;
295
296 breakpoints.update(&mut cx, |this, cx| {
297 this.toggle_breakpoint(
298 buffer,
299 BreakpointWithPosition {
300 position,
301 bp: breakpoint,
302 },
303 BreakpointEditAction::Toggle,
304 cx,
305 );
306 })?;
307 Ok(proto::Ack {})
308 }
309
310 pub(crate) fn broadcast(&self) {
311 if let Some((client, project_id)) = &self.downstream_client {
312 for (path, breakpoint_set) in &self.breakpoints {
313 let _ = client.send(proto::BreakpointsForFile {
314 project_id: *project_id,
315 path: path.to_str().map(ToOwned::to_owned).unwrap(),
316 breakpoints: breakpoint_set
317 .breakpoints
318 .iter()
319 .filter_map(|breakpoint| {
320 breakpoint.bp.bp.to_proto(
321 path,
322 breakpoint.position(),
323 &breakpoint.session_state,
324 )
325 })
326 .collect(),
327 });
328 }
329 }
330 }
331
332 pub(crate) fn update_session_breakpoint(
333 &mut self,
334 session_id: SessionId,
335 _: dap::BreakpointEventReason,
336 breakpoint: dap::Breakpoint,
337 ) {
338 maybe!({
339 let event_id = breakpoint.id?;
340
341 let state = self
342 .breakpoints
343 .values_mut()
344 .find_map(|breakpoints_in_file| {
345 breakpoints_in_file
346 .breakpoints
347 .iter_mut()
348 .find_map(|state| {
349 let state = state.session_state.get_mut(&session_id)?;
350
351 if state.id == event_id {
352 Some(state)
353 } else {
354 None
355 }
356 })
357 })?;
358
359 state.verified = breakpoint.verified;
360 Some(())
361 });
362 }
363
364 pub(super) fn mark_breakpoints_verified(
365 &mut self,
366 session_id: SessionId,
367 abs_path: &Path,
368
369 it: impl Iterator<Item = (BreakpointWithPosition, BreakpointSessionState)>,
370 ) {
371 maybe!({
372 let breakpoints = self.breakpoints.get_mut(abs_path)?;
373 for (breakpoint, state) in it {
374 if let Some(to_update) = breakpoints
375 .breakpoints
376 .iter_mut()
377 .find(|bp| *bp.position() == breakpoint.position)
378 {
379 to_update
380 .session_state
381 .entry(session_id)
382 .insert_entry(state);
383 }
384 }
385 Some(())
386 });
387 }
388
389 pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
390 worktree::File::from_dyn(buffer.read(cx).file())
391 .map(|file| file.worktree.read(cx).absolutize(&file.path))
392 .map(Arc::<Path>::from)
393 }
394
395 pub fn toggle_breakpoint(
396 &mut self,
397 buffer: Entity<Buffer>,
398 mut breakpoint: BreakpointWithPosition,
399 edit_action: BreakpointEditAction,
400 cx: &mut Context<Self>,
401 ) {
402 let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
403 return;
404 };
405
406 let breakpoint_set = self
407 .breakpoints
408 .entry(abs_path.clone())
409 .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
410
411 match edit_action {
412 BreakpointEditAction::Toggle => {
413 let len_before = breakpoint_set.breakpoints.len();
414 breakpoint_set
415 .breakpoints
416 .retain(|value| breakpoint != value.bp);
417 if len_before == breakpoint_set.breakpoints.len() {
418 // We did not remove any breakpoint, hence let's toggle one.
419 breakpoint_set
420 .breakpoints
421 .push(StatefulBreakpoint::new(breakpoint.clone()));
422 }
423 }
424 BreakpointEditAction::InvertState => {
425 if let Some(bp) = breakpoint_set
426 .breakpoints
427 .iter_mut()
428 .find(|value| breakpoint == value.bp)
429 {
430 let bp = &mut bp.bp.bp;
431 if bp.is_enabled() {
432 bp.state = BreakpointState::Disabled;
433 } else {
434 bp.state = BreakpointState::Enabled;
435 }
436 } else {
437 breakpoint.bp.state = BreakpointState::Disabled;
438 breakpoint_set
439 .breakpoints
440 .push(StatefulBreakpoint::new(breakpoint.clone()));
441 }
442 }
443 BreakpointEditAction::EditLogMessage(log_message) => {
444 if !log_message.is_empty() {
445 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| {
446 if breakpoint.position == *bp.position() {
447 Some(&mut bp.bp.bp)
448 } else {
449 None
450 }
451 });
452
453 if let Some(found_bp) = found_bp {
454 found_bp.message = Some(log_message);
455 } else {
456 breakpoint.bp.message = Some(log_message);
457 // We did not remove any breakpoint, hence let's toggle one.
458 breakpoint_set
459 .breakpoints
460 .push(StatefulBreakpoint::new(breakpoint.clone()));
461 }
462 } else if breakpoint.bp.message.is_some() {
463 if let Some(position) = breakpoint_set
464 .breakpoints
465 .iter()
466 .find_position(|other| breakpoint == other.bp)
467 .map(|res| res.0)
468 {
469 breakpoint_set.breakpoints.remove(position);
470 } else {
471 log::error!("Failed to find position of breakpoint to delete")
472 }
473 }
474 }
475 BreakpointEditAction::EditHitCondition(hit_condition) => {
476 if !hit_condition.is_empty() {
477 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
478 if breakpoint.position == *other.position() {
479 Some(&mut other.bp.bp)
480 } else {
481 None
482 }
483 });
484
485 if let Some(found_bp) = found_bp {
486 found_bp.hit_condition = Some(hit_condition);
487 } else {
488 breakpoint.bp.hit_condition = Some(hit_condition);
489 // We did not remove any breakpoint, hence let's toggle one.
490 breakpoint_set
491 .breakpoints
492 .push(StatefulBreakpoint::new(breakpoint.clone()))
493 }
494 } else if breakpoint.bp.hit_condition.is_some() {
495 if let Some(position) = breakpoint_set
496 .breakpoints
497 .iter()
498 .find_position(|bp| breakpoint == bp.bp)
499 .map(|res| res.0)
500 {
501 breakpoint_set.breakpoints.remove(position);
502 } else {
503 log::error!("Failed to find position of breakpoint to delete")
504 }
505 }
506 }
507 BreakpointEditAction::EditCondition(condition) => {
508 if !condition.is_empty() {
509 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
510 if breakpoint.position == *other.position() {
511 Some(&mut other.bp.bp)
512 } else {
513 None
514 }
515 });
516
517 if let Some(found_bp) = found_bp {
518 found_bp.condition = Some(condition);
519 } else {
520 breakpoint.bp.condition = Some(condition);
521 // We did not remove any breakpoint, hence let's toggle one.
522 breakpoint_set
523 .breakpoints
524 .push(StatefulBreakpoint::new(breakpoint.clone()));
525 }
526 } else if breakpoint.bp.condition.is_some() {
527 if let Some(position) = breakpoint_set
528 .breakpoints
529 .iter()
530 .find_position(|bp| breakpoint == bp.bp)
531 .map(|res| res.0)
532 {
533 breakpoint_set.breakpoints.remove(position);
534 } else {
535 log::error!("Failed to find position of breakpoint to delete")
536 }
537 }
538 }
539 }
540
541 if breakpoint_set.breakpoints.is_empty() {
542 self.breakpoints.remove(&abs_path);
543 }
544 if let BreakpointStoreMode::Remote(remote) = &self.mode {
545 if let Some(breakpoint) =
546 breakpoint
547 .bp
548 .to_proto(&abs_path, &breakpoint.position, &HashMap::default())
549 {
550 cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
551 project_id: remote._upstream_project_id,
552 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
553 breakpoint: Some(breakpoint),
554 }))
555 .detach();
556 }
557 } else if let Some((client, project_id)) = &self.downstream_client {
558 let breakpoints = self
559 .breakpoints
560 .get(&abs_path)
561 .map(|breakpoint_set| {
562 breakpoint_set
563 .breakpoints
564 .iter()
565 .filter_map(|bp| {
566 bp.bp
567 .bp
568 .to_proto(&abs_path, bp.position(), &bp.session_state)
569 })
570 .collect()
571 })
572 .unwrap_or_default();
573
574 let _ = client.send(proto::BreakpointsForFile {
575 project_id: *project_id,
576 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
577 breakpoints,
578 });
579 }
580
581 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
582 abs_path,
583 BreakpointUpdatedReason::Toggled,
584 ));
585 cx.notify();
586 }
587
588 pub fn on_file_rename(
589 &mut self,
590 old_path: Arc<Path>,
591 new_path: Arc<Path>,
592 cx: &mut Context<Self>,
593 ) {
594 if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
595 self.breakpoints.insert(new_path, breakpoints);
596
597 cx.notify();
598 }
599 }
600
601 pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
602 let breakpoint_paths = self.breakpoints.keys().cloned().collect();
603 self.breakpoints.clear();
604 cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
605 }
606
607 pub fn breakpoints<'a>(
608 &'a self,
609 buffer: &'a Entity<Buffer>,
610 range: Option<Range<text::Anchor>>,
611 buffer_snapshot: &'a BufferSnapshot,
612 cx: &App,
613 ) -> impl Iterator<Item = (&'a BreakpointWithPosition, Option<BreakpointSessionState>)> + 'a
614 {
615 let abs_path = Self::abs_path_from_buffer(buffer, cx);
616 let active_session_id = self
617 .active_stack_frame
618 .as_ref()
619 .map(|frame| frame.session_id);
620 abs_path
621 .and_then(|path| self.breakpoints.get(&path))
622 .into_iter()
623 .flat_map(move |file_breakpoints| {
624 file_breakpoints.breakpoints.iter().filter_map({
625 let range = range.clone();
626 move |bp| {
627 if let Some(range) = &range
628 && (bp.position().cmp(&range.start, buffer_snapshot).is_lt()
629 || bp.position().cmp(&range.end, buffer_snapshot).is_gt())
630 {
631 return None;
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,
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 for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
836 (path.to_string_lossy(), bp_in_file.breakpoints.len())
837 }) {
838 let breakpoint_str = if count > 1 {
839 "breakpoints"
840 } else {
841 "breakpoint"
842 };
843 log::debug!("Deserialized {count} {breakpoint_str} at path: {path}");
844 }
845
846 this.breakpoints = new_breakpoints;
847
848 cx.notify();
849 })?;
850
851 Ok(())
852 })
853 } else {
854 Task::ready(Ok(()))
855 }
856 }
857
858 #[cfg(any(test, feature = "test-support"))]
859 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
860 self.breakpoints.keys().cloned().collect()
861 }
862}
863
864#[derive(Clone, Copy)]
865pub enum BreakpointUpdatedReason {
866 Toggled,
867 FileSaved,
868}
869
870pub enum BreakpointStoreEvent {
871 SetDebugLine,
872 ClearDebugLines,
873 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
874 BreakpointsCleared(Vec<Arc<Path>>),
875}
876
877impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
878
879type BreakpointMessage = Arc<str>;
880
881#[derive(Clone, Debug)]
882pub enum BreakpointEditAction {
883 Toggle,
884 InvertState,
885 EditLogMessage(BreakpointMessage),
886 EditCondition(BreakpointMessage),
887 EditHitCondition(BreakpointMessage),
888}
889
890#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
891pub enum BreakpointState {
892 Enabled,
893 Disabled,
894}
895
896impl BreakpointState {
897 #[inline]
898 pub fn is_enabled(&self) -> bool {
899 matches!(self, BreakpointState::Enabled)
900 }
901
902 #[inline]
903 pub fn is_disabled(&self) -> bool {
904 matches!(self, BreakpointState::Disabled)
905 }
906
907 #[inline]
908 pub fn to_int(self) -> i32 {
909 match self {
910 BreakpointState::Enabled => 0,
911 BreakpointState::Disabled => 1,
912 }
913 }
914}
915
916#[derive(Clone, Debug, Hash, PartialEq, Eq)]
917pub struct Breakpoint {
918 pub message: Option<BreakpointMessage>,
919 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
920 pub hit_condition: Option<Arc<str>>,
921 pub condition: Option<BreakpointMessage>,
922 pub state: BreakpointState,
923}
924
925impl Breakpoint {
926 pub fn new_standard() -> Self {
927 Self {
928 state: BreakpointState::Enabled,
929 hit_condition: None,
930 condition: None,
931 message: None,
932 }
933 }
934
935 pub fn new_condition(hit_condition: &str) -> Self {
936 Self {
937 state: BreakpointState::Enabled,
938 condition: None,
939 hit_condition: Some(hit_condition.into()),
940 message: None,
941 }
942 }
943
944 pub fn new_log(log_message: &str) -> Self {
945 Self {
946 state: BreakpointState::Enabled,
947 hit_condition: None,
948 condition: None,
949 message: Some(log_message.into()),
950 }
951 }
952
953 fn to_proto(
954 &self,
955 _path: &Path,
956 position: &text::Anchor,
957 session_states: &HashMap<SessionId, BreakpointSessionState>,
958 ) -> Option<client::proto::Breakpoint> {
959 Some(client::proto::Breakpoint {
960 position: Some(serialize_text_anchor(position)),
961 state: match self.state {
962 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
963 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
964 },
965 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
966 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
967 hit_condition: self
968 .hit_condition
969 .as_ref()
970 .map(|s| String::from(s.as_ref())),
971 session_state: session_states
972 .iter()
973 .map(|(session_id, state)| {
974 (
975 session_id.to_proto(),
976 proto::BreakpointSessionState {
977 id: state.id,
978 verified: state.verified,
979 },
980 )
981 })
982 .collect(),
983 })
984 }
985
986 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
987 Some(Self {
988 state: match proto::BreakpointState::from_i32(breakpoint.state) {
989 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
990 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
991 },
992 message: breakpoint.message.map(Into::into),
993 condition: breakpoint.condition.map(Into::into),
994 hit_condition: breakpoint.hit_condition.map(Into::into),
995 })
996 }
997
998 #[inline]
999 pub fn is_enabled(&self) -> bool {
1000 self.state.is_enabled()
1001 }
1002
1003 #[inline]
1004 pub fn is_disabled(&self) -> bool {
1005 self.state.is_disabled()
1006 }
1007}
1008
1009/// Breakpoint for location within source code.
1010#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1011pub struct SourceBreakpoint {
1012 pub row: u32,
1013 pub path: Arc<Path>,
1014 pub message: Option<Arc<str>>,
1015 pub condition: Option<Arc<str>>,
1016 pub hit_condition: Option<Arc<str>>,
1017 pub state: BreakpointState,
1018}
1019
1020impl From<SourceBreakpoint> for dap::SourceBreakpoint {
1021 fn from(bp: SourceBreakpoint) -> Self {
1022 Self {
1023 line: bp.row as u64 + 1,
1024 column: None,
1025 condition: bp
1026 .condition
1027 .map(|condition| String::from(condition.as_ref())),
1028 hit_condition: bp
1029 .hit_condition
1030 .map(|hit_condition| String::from(hit_condition.as_ref())),
1031 log_message: bp.message.map(|message| String::from(message.as_ref())),
1032 mode: None,
1033 }
1034 }
1035}