Skip to repository content1083 lines · 39.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:50:30.754Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
breakpoint_store.rs
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::{Bias, Point, PointUtf16, Unclipped};
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.clone(), cx));
419
420 // Buffers changed for the file, migrate breakpoints to the new buffer
421 if breakpoint_set.buffer != buffer {
422 let old_snapshot = breakpoint_set.buffer.read(cx).snapshot();
423 let new_snapshot = buffer.read(cx).snapshot();
424 let breakpoints = breakpoint_set
425 .breakpoints
426 .drain(..)
427 .map(|mut breakpoint| {
428 let old_position =
429 old_snapshot.summary_for_anchor::<PointUtf16>(breakpoint.position());
430 let new_position = PointUtf16::new(old_position.row, 0);
431 let new_position =
432 new_snapshot.clip_point_utf16(Unclipped(new_position), Bias::Left);
433 breakpoint.bp.position = new_snapshot.anchor_after(new_position);
434 breakpoint
435 })
436 .collect();
437 *breakpoint_set = BreakpointsInFile::new(buffer, cx);
438 breakpoint_set.breakpoints = breakpoints;
439 }
440
441 match edit_action {
442 BreakpointEditAction::Toggle => {
443 let len_before = breakpoint_set.breakpoints.len();
444 breakpoint_set
445 .breakpoints
446 .retain(|value| breakpoint != value.bp);
447 if len_before == breakpoint_set.breakpoints.len() {
448 // We did not remove any breakpoint, hence let's toggle one.
449 breakpoint_set
450 .breakpoints
451 .push(StatefulBreakpoint::new(breakpoint.clone()));
452 }
453 }
454 BreakpointEditAction::InvertState => {
455 if let Some(bp) = breakpoint_set
456 .breakpoints
457 .iter_mut()
458 .find(|value| breakpoint == value.bp)
459 {
460 let bp = &mut bp.bp.bp;
461 if bp.is_enabled() {
462 bp.state = BreakpointState::Disabled;
463 } else {
464 bp.state = BreakpointState::Enabled;
465 }
466 } else {
467 breakpoint.bp.state = BreakpointState::Disabled;
468 breakpoint_set
469 .breakpoints
470 .push(StatefulBreakpoint::new(breakpoint.clone()));
471 }
472 }
473 BreakpointEditAction::EditLogMessage(log_message) => {
474 if !log_message.is_empty() {
475 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| {
476 if breakpoint.position == *bp.position() {
477 Some(&mut bp.bp.bp)
478 } else {
479 None
480 }
481 });
482
483 if let Some(found_bp) = found_bp {
484 found_bp.message = Some(log_message);
485 } else {
486 breakpoint.bp.message = Some(log_message);
487 // We did not remove any breakpoint, hence let's toggle one.
488 breakpoint_set
489 .breakpoints
490 .push(StatefulBreakpoint::new(breakpoint.clone()));
491 }
492 } else if breakpoint.bp.message.is_some() {
493 if let Some(position) = breakpoint_set
494 .breakpoints
495 .iter()
496 .find_position(|other| breakpoint == other.bp)
497 .map(|res| res.0)
498 {
499 breakpoint_set.breakpoints.remove(position);
500 } else {
501 log::error!("Failed to find position of breakpoint to delete")
502 }
503 }
504 }
505 BreakpointEditAction::EditHitCondition(hit_condition) => {
506 if !hit_condition.is_empty() {
507 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
508 if breakpoint.position == *other.position() {
509 Some(&mut other.bp.bp)
510 } else {
511 None
512 }
513 });
514
515 if let Some(found_bp) = found_bp {
516 found_bp.hit_condition = Some(hit_condition);
517 } else {
518 breakpoint.bp.hit_condition = Some(hit_condition);
519 // We did not remove any breakpoint, hence let's toggle one.
520 breakpoint_set
521 .breakpoints
522 .push(StatefulBreakpoint::new(breakpoint.clone()))
523 }
524 } else if breakpoint.bp.hit_condition.is_some() {
525 if let Some(position) = breakpoint_set
526 .breakpoints
527 .iter()
528 .find_position(|bp| breakpoint == bp.bp)
529 .map(|res| res.0)
530 {
531 breakpoint_set.breakpoints.remove(position);
532 } else {
533 log::error!("Failed to find position of breakpoint to delete")
534 }
535 }
536 }
537 BreakpointEditAction::EditCondition(condition) => {
538 if !condition.is_empty() {
539 let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
540 if breakpoint.position == *other.position() {
541 Some(&mut other.bp.bp)
542 } else {
543 None
544 }
545 });
546
547 if let Some(found_bp) = found_bp {
548 found_bp.condition = Some(condition);
549 } else {
550 breakpoint.bp.condition = Some(condition);
551 // We did not remove any breakpoint, hence let's toggle one.
552 breakpoint_set
553 .breakpoints
554 .push(StatefulBreakpoint::new(breakpoint.clone()));
555 }
556 } else if breakpoint.bp.condition.is_some() {
557 if let Some(position) = breakpoint_set
558 .breakpoints
559 .iter()
560 .find_position(|bp| breakpoint == bp.bp)
561 .map(|res| res.0)
562 {
563 breakpoint_set.breakpoints.remove(position);
564 } else {
565 log::error!("Failed to find position of breakpoint to delete")
566 }
567 }
568 }
569 }
570
571 if breakpoint_set.breakpoints.is_empty() {
572 self.breakpoints.remove(&abs_path);
573 }
574 if let BreakpointStoreMode::Remote(remote) = &self.mode {
575 if let Some(breakpoint) =
576 breakpoint
577 .bp
578 .to_proto(&abs_path, &breakpoint.position, &HashMap::default())
579 {
580 cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
581 project_id: remote.upstream_project_id,
582 path: abs_path.to_string_lossy().into_owned(),
583 breakpoint: Some(breakpoint),
584 }))
585 .detach();
586 }
587 } else if let Some((client, project_id)) = &self.downstream_client {
588 let breakpoints = self
589 .breakpoints
590 .get(&abs_path)
591 .map(|breakpoint_set| {
592 breakpoint_set
593 .breakpoints
594 .iter()
595 .filter_map(|bp| {
596 bp.bp
597 .bp
598 .to_proto(&abs_path, bp.position(), &bp.session_state)
599 })
600 .collect()
601 })
602 .unwrap_or_default();
603
604 let _ = client.send(proto::BreakpointsForFile {
605 project_id: *project_id,
606 path: abs_path.to_string_lossy().into_owned(),
607 breakpoints,
608 });
609 }
610
611 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
612 abs_path,
613 BreakpointUpdatedReason::Toggled,
614 ));
615 cx.notify();
616 }
617
618 pub fn on_file_rename(
619 &mut self,
620 old_path: Arc<Path>,
621 new_path: Arc<Path>,
622 cx: &mut Context<Self>,
623 ) {
624 if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
625 self.breakpoints.insert(new_path, breakpoints);
626
627 cx.notify();
628 }
629 }
630
631 pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
632 let breakpoint_paths = self.breakpoints.keys().cloned().collect();
633 self.breakpoints.clear();
634 cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
635 }
636
637 pub fn breakpoints<'a>(
638 &'a self,
639 buffer: &'a Entity<Buffer>,
640 range: Option<Range<text::Anchor>>,
641 buffer_snapshot: &'a BufferSnapshot,
642 cx: &App,
643 ) -> impl Iterator<Item = (&'a BreakpointWithPosition, Option<BreakpointSessionState>)> + 'a
644 {
645 let abs_path = Self::abs_path_from_buffer(buffer, cx);
646 let active_session_id = self
647 .active_stack_frame
648 .as_ref()
649 .map(|frame| frame.session_id);
650 abs_path
651 .and_then(|path| self.breakpoints.get(&path))
652 .into_iter()
653 .flat_map(move |file_breakpoints| {
654 file_breakpoints.breakpoints.iter().filter_map({
655 let range = range.clone();
656 move |bp| {
657 if !buffer_snapshot.can_resolve(bp.position()) {
658 return None;
659 }
660
661 if let Some(range) = &range
662 && (bp.position().cmp(&range.start, buffer_snapshot).is_lt()
663 || bp.position().cmp(&range.end, buffer_snapshot).is_gt())
664 {
665 return None;
666 }
667 let session_state = active_session_id
668 .and_then(|id| bp.session_state.get(&id))
669 .copied();
670 Some((&bp.bp, session_state))
671 }
672 })
673 })
674 }
675
676 pub fn active_position(&self) -> Option<&ActiveStackFrame> {
677 self.active_stack_frame.as_ref()
678 }
679
680 pub fn active_debug_line_pane_id(&self) -> Option<EntityId> {
681 self.active_debug_line_pane_id
682 }
683
684 pub fn set_active_debug_pane_id(&mut self, pane_id: EntityId) {
685 self.active_debug_line_pane_id = Some(pane_id);
686 }
687
688 pub fn remove_active_position(
689 &mut self,
690 session_id: Option<SessionId>,
691 cx: &mut Context<Self>,
692 ) {
693 if let Some(session_id) = session_id {
694 if self
695 .active_stack_frame
696 .take_if(|active_stack_frame| active_stack_frame.session_id == session_id)
697 .is_some()
698 {
699 self.active_debug_line_pane_id = None;
700 }
701 } else {
702 self.active_stack_frame.take();
703 self.active_debug_line_pane_id = None;
704 }
705
706 cx.emit(BreakpointStoreEvent::ClearDebugLines);
707 cx.notify();
708 }
709
710 pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context<Self>) {
711 if self
712 .active_stack_frame
713 .as_ref()
714 .is_some_and(|active_position| active_position == &position)
715 {
716 cx.emit(BreakpointStoreEvent::SetDebugLine);
717 return;
718 }
719
720 if self.active_stack_frame.is_some() {
721 cx.emit(BreakpointStoreEvent::ClearDebugLines);
722 }
723
724 self.active_stack_frame = Some(position);
725
726 cx.emit(BreakpointStoreEvent::SetDebugLine);
727 cx.notify();
728 }
729
730 pub fn breakpoint_at_row(
731 &self,
732 path: &Path,
733 row: u32,
734 cx: &App,
735 ) -> Option<(Entity<Buffer>, BreakpointWithPosition)> {
736 self.breakpoints.get(path).and_then(|breakpoints| {
737 let snapshot = breakpoints.buffer.read(cx).text_snapshot();
738
739 breakpoints
740 .breakpoints
741 .iter()
742 .find(|bp| bp.position().summary::<Point>(&snapshot).row == row)
743 .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone()))
744 })
745 }
746
747 pub fn breakpoints_from_path(&self, path: &Arc<Path>) -> Vec<BreakpointWithPosition> {
748 self.breakpoints
749 .get(path)
750 .map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect())
751 .unwrap_or_default()
752 }
753
754 pub fn source_breakpoints_from_path(
755 &self,
756 path: &Arc<Path>,
757 cx: &App,
758 ) -> Vec<SourceBreakpoint> {
759 self.breakpoints
760 .get(path)
761 .map(|bp| {
762 let snapshot = bp.buffer.read(cx).snapshot();
763 bp.breakpoints
764 .iter()
765 .map(|bp| {
766 let position = snapshot.summary_for_anchor::<PointUtf16>(bp.position()).row;
767 let bp = &bp.bp;
768 SourceBreakpoint {
769 row: position,
770 path: path.clone(),
771 state: bp.bp.state,
772 message: bp.bp.message.clone(),
773 condition: bp.bp.condition.clone(),
774 hit_condition: bp.bp.hit_condition.clone(),
775 }
776 })
777 .collect()
778 })
779 .unwrap_or_default()
780 }
781
782 pub fn all_breakpoints(&self) -> BTreeMap<Arc<Path>, Vec<BreakpointWithPosition>> {
783 self.breakpoints
784 .iter()
785 .map(|(path, bp)| {
786 (
787 path.clone(),
788 bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(),
789 )
790 })
791 .collect()
792 }
793 pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
794 self.breakpoints
795 .iter()
796 .map(|(path, bp)| {
797 let snapshot = bp.buffer.read(cx).snapshot();
798 (
799 path.clone(),
800 bp.breakpoints
801 .iter()
802 .map(|breakpoint| {
803 let position = snapshot
804 .summary_for_anchor::<PointUtf16>(breakpoint.position())
805 .row;
806 let breakpoint = &breakpoint.bp;
807 SourceBreakpoint {
808 row: position,
809 path: path.clone(),
810 message: breakpoint.bp.message.clone(),
811 state: breakpoint.bp.state,
812 hit_condition: breakpoint.bp.hit_condition.clone(),
813 condition: breakpoint.bp.condition.clone(),
814 }
815 })
816 .collect(),
817 )
818 })
819 .collect()
820 }
821
822 pub fn with_serialized_breakpoints(
823 &self,
824 breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
825 cx: &mut Context<BreakpointStore>,
826 ) -> Task<Result<()>> {
827 if let BreakpointStoreMode::Local = &self.mode {
828 let worktree_store = self.worktree_store.downgrade();
829 let buffer_store = self.buffer_store.downgrade();
830 cx.spawn(async move |this, cx| {
831 let mut new_breakpoints = BTreeMap::default();
832 for (path, bps) in breakpoints {
833 if bps.is_empty() {
834 continue;
835 }
836 let (worktree, relative_path) = worktree_store
837 .update(cx, |this, cx| {
838 this.find_or_create_worktree(&path, false, cx)
839 })?
840 .await?;
841 let buffer = buffer_store
842 .update(cx, |this, cx| {
843 let path = ProjectPath {
844 worktree_id: worktree.read(cx).id(),
845 path: relative_path,
846 };
847 this.open_buffer(path, cx)
848 })?
849 .await;
850 let Ok(buffer) = buffer else {
851 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
852 continue;
853 };
854 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
855
856 let mut breakpoints_for_file =
857 this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
858
859 for bp in bps {
860 let max_point = snapshot.max_point_utf16();
861 let point = PointUtf16::new(bp.row, 0);
862 if point > max_point {
863 log::error!("skipping a deserialized breakpoint that's out of range");
864 continue;
865 }
866 let position = snapshot.anchor_after(point);
867 breakpoints_for_file
868 .breakpoints
869 .push(StatefulBreakpoint::new(BreakpointWithPosition {
870 position,
871 bp: Breakpoint {
872 message: bp.message,
873 state: bp.state,
874 condition: bp.condition,
875 hit_condition: bp.hit_condition,
876 },
877 }))
878 }
879 new_breakpoints.insert(path, breakpoints_for_file);
880 }
881 this.update(cx, |this, cx| {
882 for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
883 (path.to_string_lossy(), bp_in_file.breakpoints.len())
884 }) {
885 let breakpoint_str = if count > 1 {
886 "breakpoints"
887 } else {
888 "breakpoint"
889 };
890 log::debug!("Deserialized {count} {breakpoint_str} at path: {path}");
891 }
892
893 this.breakpoints = new_breakpoints;
894
895 cx.notify();
896 })?;
897
898 Ok(())
899 })
900 } else {
901 Task::ready(Ok(()))
902 }
903 }
904
905 #[cfg(any(test, feature = "test-support"))]
906 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
907 self.breakpoints.keys().cloned().collect()
908 }
909}
910
911#[derive(Clone, Copy)]
912pub enum BreakpointUpdatedReason {
913 Toggled,
914 FileSaved,
915}
916
917pub enum BreakpointStoreEvent {
918 SetDebugLine,
919 ClearDebugLines,
920 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
921 BreakpointsCleared(Vec<Arc<Path>>),
922}
923
924impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
925
926type BreakpointMessage = Arc<str>;
927
928#[derive(Clone, Debug)]
929pub enum BreakpointEditAction {
930 Toggle,
931 InvertState,
932 EditLogMessage(BreakpointMessage),
933 EditCondition(BreakpointMessage),
934 EditHitCondition(BreakpointMessage),
935}
936
937#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
938pub enum BreakpointState {
939 Enabled,
940 Disabled,
941}
942
943impl BreakpointState {
944 #[inline]
945 pub fn is_enabled(&self) -> bool {
946 matches!(self, BreakpointState::Enabled)
947 }
948
949 #[inline]
950 pub fn is_disabled(&self) -> bool {
951 matches!(self, BreakpointState::Disabled)
952 }
953
954 #[inline]
955 pub fn to_int(self) -> i32 {
956 match self {
957 BreakpointState::Enabled => 0,
958 BreakpointState::Disabled => 1,
959 }
960 }
961}
962
963#[derive(Clone, Debug, Hash, PartialEq, Eq)]
964pub struct Breakpoint {
965 pub message: Option<BreakpointMessage>,
966 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
967 pub hit_condition: Option<Arc<str>>,
968 pub condition: Option<BreakpointMessage>,
969 pub state: BreakpointState,
970}
971
972impl Breakpoint {
973 pub fn new_standard() -> Self {
974 Self {
975 state: BreakpointState::Enabled,
976 hit_condition: None,
977 condition: None,
978 message: None,
979 }
980 }
981
982 pub fn new_condition(hit_condition: &str) -> Self {
983 Self {
984 state: BreakpointState::Enabled,
985 condition: None,
986 hit_condition: Some(hit_condition.into()),
987 message: None,
988 }
989 }
990
991 pub fn new_log(log_message: &str) -> Self {
992 Self {
993 state: BreakpointState::Enabled,
994 hit_condition: None,
995 condition: None,
996 message: Some(log_message.into()),
997 }
998 }
999
1000 fn to_proto(
1001 &self,
1002 _path: &Path,
1003 position: &text::Anchor,
1004 session_states: &HashMap<SessionId, BreakpointSessionState>,
1005 ) -> Option<client::proto::Breakpoint> {
1006 Some(client::proto::Breakpoint {
1007 position: Some(serialize_text_anchor(position)),
1008 state: match self.state {
1009 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
1010 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
1011 },
1012 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
1013 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
1014 hit_condition: self
1015 .hit_condition
1016 .as_ref()
1017 .map(|s| String::from(s.as_ref())),
1018 session_state: session_states
1019 .iter()
1020 .map(|(session_id, state)| {
1021 (
1022 session_id.to_proto(),
1023 proto::BreakpointSessionState {
1024 id: state.id,
1025 verified: state.verified,
1026 },
1027 )
1028 })
1029 .collect(),
1030 })
1031 }
1032
1033 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
1034 Some(Self {
1035 state: match proto::BreakpointState::from_i32(breakpoint.state) {
1036 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
1037 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
1038 },
1039 message: breakpoint.message.map(Into::into),
1040 condition: breakpoint.condition.map(Into::into),
1041 hit_condition: breakpoint.hit_condition.map(Into::into),
1042 })
1043 }
1044
1045 #[inline]
1046 pub fn is_enabled(&self) -> bool {
1047 self.state.is_enabled()
1048 }
1049
1050 #[inline]
1051 pub fn is_disabled(&self) -> bool {
1052 self.state.is_disabled()
1053 }
1054}
1055
1056/// Breakpoint for location within source code.
1057#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1058pub struct SourceBreakpoint {
1059 pub row: u32,
1060 pub path: Arc<Path>,
1061 pub message: Option<Arc<str>>,
1062 pub condition: Option<Arc<str>>,
1063 pub hit_condition: Option<Arc<str>>,
1064 pub state: BreakpointState,
1065}
1066
1067impl From<SourceBreakpoint> for dap::SourceBreakpoint {
1068 fn from(bp: SourceBreakpoint) -> Self {
1069 Self {
1070 line: bp.row as u64 + 1,
1071 column: None,
1072 condition: bp
1073 .condition
1074 .map(|condition| String::from(condition.as_ref())),
1075 hit_condition: bp
1076 .hit_condition
1077 .map(|hit_condition| String::from(hit_condition.as_ref())),
1078 log_message: bp.message.map(|message| String::from(message.as_ref())),
1079 mode: None,
1080 }
1081 }
1082}
1083