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