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