Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:56:31.804Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

breakpoint_store.rs

1083 lines · 39.2 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.