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::{anyhow, Result};
  5use breakpoints_in_file::BreakpointsInFile;
  6use collections::BTreeMap;
  7use dap::client::SessionId;
  8use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
  9use language::{proto::serialize_anchor as serialize_text_anchor, Buffer, BufferSnapshot};
 10use rpc::{
 11    proto::{self},
 12    AnyProtoClient, TypedEnvelope,
 13};
 14use std::{
 15    hash::{Hash, Hasher},
 16    ops::Range,
 17    path::Path,
 18    sync::Arc,
 19};
 20use text::{Point, PointUtf16};
 21
 22use crate::{buffer_store::BufferStore, worktree_store::WorktreeStore, Project, ProjectPath};
 23
 24mod breakpoints_in_file {
 25    use language::BufferEvent;
 26
 27    use super::*;
 28
 29    #[derive(Clone)]
 30    pub(super) struct BreakpointsInFile {
 31        pub(super) buffer: Entity<Buffer>,
 32        // 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
 33        pub(super) breakpoints: Vec<(text::Anchor, Breakpoint)>,
 34        _subscription: Arc<Subscription>,
 35    }
 36
 37    impl BreakpointsInFile {
 38        pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {
 39            let subscription =
 40                Arc::from(cx.subscribe(&buffer, |_, buffer, event, cx| match event {
 41                    BufferEvent::Saved => {
 42                        if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
 43                            cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
 44                                abs_path,
 45                                BreakpointUpdatedReason::FileSaved,
 46                            ));
 47                        }
 48                    }
 49                    _ => {}
 50                }));
 51
 52            BreakpointsInFile {
 53                buffer,
 54                breakpoints: Vec::new(),
 55                _subscription: subscription,
 56            }
 57        }
 58    }
 59}
 60
 61#[derive(Clone)]
 62struct RemoteBreakpointStore {
 63    upstream_client: AnyProtoClient,
 64    _upstream_project_id: u64,
 65}
 66
 67#[derive(Clone)]
 68struct LocalBreakpointStore {
 69    worktree_store: Entity<WorktreeStore>,
 70    buffer_store: Entity<BufferStore>,
 71}
 72
 73#[derive(Clone)]
 74enum BreakpointStoreMode {
 75    Local(LocalBreakpointStore),
 76    Remote(RemoteBreakpointStore),
 77}
 78pub struct BreakpointStore {
 79    breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,
 80    downstream_client: Option<(AnyProtoClient, u64)>,
 81    active_stack_frame: Option<(SessionId, Arc<Path>, text::Anchor)>,
 82    // E.g ssh
 83    mode: BreakpointStoreMode,
 84}
 85
 86impl BreakpointStore {
 87    pub fn init(client: &AnyProtoClient) {
 88        client.add_entity_request_handler(Self::handle_toggle_breakpoint);
 89        client.add_entity_message_handler(Self::handle_breakpoints_for_file);
 90    }
 91    pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
 92        BreakpointStore {
 93            breakpoints: BTreeMap::new(),
 94            mode: BreakpointStoreMode::Local(LocalBreakpointStore {
 95                worktree_store,
 96                buffer_store,
 97            }),
 98            downstream_client: None,
 99            active_stack_frame: Default::default(),
100        }
101    }
102
103    pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
104        BreakpointStore {
105            breakpoints: BTreeMap::new(),
106            mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
107                upstream_client,
108                _upstream_project_id: upstream_project_id,
109            }),
110            downstream_client: None,
111            active_stack_frame: Default::default(),
112        }
113    }
114
115    pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
116        self.downstream_client = Some((downstream_client.clone(), project_id));
117    }
118
119    pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) {
120        self.downstream_client.take();
121
122        cx.notify();
123    }
124
125    async fn handle_breakpoints_for_file(
126        this: Entity<Project>,
127        message: TypedEnvelope<proto::BreakpointsForFile>,
128        mut cx: AsyncApp,
129    ) -> Result<()> {
130        let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
131        if message.payload.breakpoints.is_empty() {
132            return Ok(());
133        }
134
135        let buffer = this
136            .update(&mut cx, |this, cx| {
137                let path =
138                    this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
139                Some(this.open_buffer(path, cx))
140            })
141            .ok()
142            .flatten()
143            .ok_or_else(|| anyhow!("Invalid project path"))?
144            .await?;
145
146        breakpoints.update(&mut cx, move |this, cx| {
147            let bps = this
148                .breakpoints
149                .entry(Arc::<Path>::from(message.payload.path.as_ref()))
150                .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
151
152            bps.breakpoints = message
153                .payload
154                .breakpoints
155                .into_iter()
156                .filter_map(|breakpoint| {
157                    let anchor = language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
158                    let breakpoint = Breakpoint::from_proto(breakpoint)?;
159                    Some((anchor, breakpoint))
160                })
161                .collect();
162
163            cx.notify();
164        })?;
165
166        Ok(())
167    }
168
169    async fn handle_toggle_breakpoint(
170        this: Entity<Project>,
171        message: TypedEnvelope<proto::ToggleBreakpoint>,
172        mut cx: AsyncApp,
173    ) -> Result<proto::Ack> {
174        let breakpoints = this.update(&mut cx, |this, _| this.breakpoint_store())?;
175        let path = this
176            .update(&mut cx, |this, cx| {
177                this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)
178            })?
179            .ok_or_else(|| anyhow!("Could not resolve provided abs path"))?;
180        let buffer = this
181            .update(&mut cx, |this, cx| {
182                this.buffer_store().read(cx).get_by_path(&path, cx)
183            })?
184            .ok_or_else(|| anyhow!("Could not find buffer for a given path"))?;
185        let breakpoint = message
186            .payload
187            .breakpoint
188            .ok_or_else(|| anyhow!("Breakpoint not present in RPC payload"))?;
189        let anchor = language::proto::deserialize_anchor(
190            breakpoint
191                .position
192                .clone()
193                .ok_or_else(|| anyhow!("Anchor not present in RPC payload"))?,
194        )
195        .ok_or_else(|| anyhow!("Anchor deserialization failed"))?;
196        let breakpoint = Breakpoint::from_proto(breakpoint)
197            .ok_or_else(|| anyhow!("Could not deserialize breakpoint"))?;
198
199        breakpoints.update(&mut cx, |this, cx| {
200            this.toggle_breakpoint(
201                buffer,
202                (anchor, breakpoint),
203                BreakpointEditAction::Toggle,
204                cx,
205            );
206        })?;
207        Ok(proto::Ack {})
208    }
209
210    pub(crate) fn broadcast(&self) {
211        if let Some((client, project_id)) = &self.downstream_client {
212            for (path, breakpoint_set) in &self.breakpoints {
213                let _ = client.send(proto::BreakpointsForFile {
214                    project_id: *project_id,
215                    path: path.to_str().map(ToOwned::to_owned).unwrap(),
216                    breakpoints: breakpoint_set
217                        .breakpoints
218                        .iter()
219                        .filter_map(|(anchor, bp)| bp.to_proto(&path, anchor))
220                        .collect(),
221                });
222            }
223        }
224    }
225
226    fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
227        worktree::File::from_dyn(buffer.read(cx).file())
228            .and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
229            .map(Arc::<Path>::from)
230    }
231
232    pub fn toggle_breakpoint(
233        &mut self,
234        buffer: Entity<Buffer>,
235        mut breakpoint: (text::Anchor, Breakpoint),
236        edit_action: BreakpointEditAction,
237        cx: &mut Context<Self>,
238    ) {
239        let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
240            return;
241        };
242
243        let breakpoint_set = self
244            .breakpoints
245            .entry(abs_path.clone())
246            .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
247
248        match edit_action {
249            BreakpointEditAction::Toggle => {
250                let len_before = breakpoint_set.breakpoints.len();
251                breakpoint_set
252                    .breakpoints
253                    .retain(|value| &breakpoint != value);
254                if len_before == breakpoint_set.breakpoints.len() {
255                    // We did not remove any breakpoint, hence let's toggle one.
256                    breakpoint_set.breakpoints.push(breakpoint.clone());
257                }
258            }
259            BreakpointEditAction::InvertState => {
260                if let Some((_, bp)) = breakpoint_set
261                    .breakpoints
262                    .iter_mut()
263                    .find(|value| breakpoint == **value)
264                {
265                    if bp.is_enabled() {
266                        bp.state = BreakpointState::Disabled;
267                    } else {
268                        bp.state = BreakpointState::Enabled;
269                    }
270                } else {
271                    breakpoint.1.state = BreakpointState::Disabled;
272                    breakpoint_set.breakpoints.push(breakpoint.clone());
273                }
274            }
275            BreakpointEditAction::EditLogMessage(log_message) => {
276                if !log_message.is_empty() {
277                    breakpoint.1.kind = BreakpointKind::Log(log_message.clone());
278
279                    let found_bp =
280                        breakpoint_set
281                            .breakpoints
282                            .iter_mut()
283                            .find_map(|(other_pos, other_bp)| {
284                                if breakpoint.0 == *other_pos {
285                                    Some(other_bp)
286                                } else {
287                                    None
288                                }
289                            });
290
291                    if let Some(found_bp) = found_bp {
292                        found_bp.kind = BreakpointKind::Log(log_message.clone());
293                    } else {
294                        // We did not remove any breakpoint, hence let's toggle one.
295                        breakpoint_set.breakpoints.push(breakpoint.clone());
296                    }
297                } else if matches!(&breakpoint.1.kind, BreakpointKind::Log(_)) {
298                    breakpoint_set
299                        .breakpoints
300                        .retain(|(other_pos, other_kind)| {
301                            &breakpoint.0 != other_pos
302                                && matches!(other_kind.kind, BreakpointKind::Standard)
303                        });
304                }
305            }
306        }
307
308        if breakpoint_set.breakpoints.is_empty() {
309            self.breakpoints.remove(&abs_path);
310        }
311        if let BreakpointStoreMode::Remote(remote) = &self.mode {
312            if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
313                cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
314                    project_id: remote._upstream_project_id,
315                    path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
316                    breakpoint: Some(breakpoint),
317                }))
318                .detach();
319            }
320        } else if let Some((client, project_id)) = &self.downstream_client {
321            let breakpoints = self
322                .breakpoints
323                .get(&abs_path)
324                .map(|breakpoint_set| {
325                    breakpoint_set
326                        .breakpoints
327                        .iter()
328                        .filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
329                        .collect()
330                })
331                .unwrap_or_default();
332
333            let _ = client.send(proto::BreakpointsForFile {
334                project_id: *project_id,
335                path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
336                breakpoints,
337            });
338        }
339
340        cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
341            abs_path,
342            BreakpointUpdatedReason::Toggled,
343        ));
344        cx.notify();
345    }
346
347    pub fn on_file_rename(
348        &mut self,
349        old_path: Arc<Path>,
350        new_path: Arc<Path>,
351        cx: &mut Context<Self>,
352    ) {
353        if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
354            self.breakpoints.insert(new_path.clone(), breakpoints);
355
356            cx.notify();
357        }
358    }
359
360    pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
361        let breakpoint_paths = self.breakpoints.keys().cloned().collect();
362        self.breakpoints.clear();
363        cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
364    }
365
366    pub fn breakpoints<'a>(
367        &'a self,
368        buffer: &'a Entity<Buffer>,
369        range: Option<Range<text::Anchor>>,
370        buffer_snapshot: &'a BufferSnapshot,
371        cx: &App,
372    ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
373        let abs_path = Self::abs_path_from_buffer(buffer, cx);
374        abs_path
375            .and_then(|path| self.breakpoints.get(&path))
376            .into_iter()
377            .flat_map(move |file_breakpoints| {
378                file_breakpoints.breakpoints.iter().filter({
379                    let range = range.clone();
380                    move |(position, _)| {
381                        if let Some(range) = &range {
382                            position.cmp(&range.start, buffer_snapshot).is_ge()
383                                && position.cmp(&range.end, buffer_snapshot).is_le()
384                        } else {
385                            true
386                        }
387                    }
388                })
389            })
390    }
391
392    pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, text::Anchor)> {
393        self.active_stack_frame.as_ref()
394    }
395
396    pub fn remove_active_position(
397        &mut self,
398        session_id: Option<SessionId>,
399        cx: &mut Context<Self>,
400    ) {
401        if let Some(session_id) = session_id {
402            self.active_stack_frame
403                .take_if(|(id, _, _)| *id == session_id);
404        } else {
405            self.active_stack_frame.take();
406        }
407
408        cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
409        cx.notify();
410    }
411
412    pub fn set_active_position(
413        &mut self,
414        position: (SessionId, Arc<Path>, text::Anchor),
415        cx: &mut Context<Self>,
416    ) {
417        self.active_stack_frame = Some(position);
418        cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
419        cx.notify();
420    }
421
422    pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SerializedBreakpoint> {
423        self.breakpoints
424            .get(path)
425            .map(|bp| {
426                let snapshot = bp.buffer.read(cx).snapshot();
427                bp.breakpoints
428                    .iter()
429                    .map(|(position, breakpoint)| {
430                        let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
431                        SerializedBreakpoint {
432                            position,
433                            path: path.clone(),
434                            kind: breakpoint.kind.clone(),
435                            state: breakpoint.state,
436                        }
437                    })
438                    .collect()
439            })
440            .unwrap_or_default()
441    }
442
443    pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> {
444        self.breakpoints
445            .iter()
446            .map(|(path, bp)| {
447                let snapshot = bp.buffer.read(cx).snapshot();
448                (
449                    path.clone(),
450                    bp.breakpoints
451                        .iter()
452                        .map(|(position, breakpoint)| {
453                            let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
454                            SerializedBreakpoint {
455                                position,
456                                path: path.clone(),
457                                kind: breakpoint.kind.clone(),
458                                state: breakpoint.state,
459                            }
460                        })
461                        .collect(),
462                )
463            })
464            .collect()
465    }
466
467    pub fn with_serialized_breakpoints(
468        &self,
469        breakpoints: BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
470        cx: &mut Context<BreakpointStore>,
471    ) -> Task<Result<()>> {
472        if let BreakpointStoreMode::Local(mode) = &self.mode {
473            let mode = mode.clone();
474            cx.spawn(async move |this, cx| {
475                let mut new_breakpoints = BTreeMap::default();
476                for (path, bps) in breakpoints {
477                    if bps.is_empty() {
478                        continue;
479                    }
480                    let (worktree, relative_path) = mode
481                        .worktree_store
482                        .update(cx, |this, cx| {
483                            this.find_or_create_worktree(&path, false, cx)
484                        })?
485                        .await?;
486                    let buffer = mode
487                        .buffer_store
488                        .update(cx, |this, cx| {
489                            let path = ProjectPath {
490                                worktree_id: worktree.read(cx).id(),
491                                path: relative_path.into(),
492                            };
493                            this.open_buffer(path, cx)
494                        })?
495                        .await;
496                    let Ok(buffer) = buffer else {
497                        log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
498                        continue;
499                    };
500                    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
501
502                    let mut breakpoints_for_file =
503                        this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
504
505                    for bp in bps {
506                        let position = snapshot.anchor_after(Point::new(bp.position, 0));
507                        breakpoints_for_file.breakpoints.push((
508                            position,
509                            Breakpoint {
510                                kind: bp.kind,
511                                state: bp.state,
512                            },
513                        ))
514                    }
515                    new_breakpoints.insert(path, breakpoints_for_file);
516                }
517                this.update(cx, |this, cx| {
518                    this.breakpoints = new_breakpoints;
519                    cx.notify();
520                })?;
521
522                Ok(())
523            })
524        } else {
525            Task::ready(Ok(()))
526        }
527    }
528
529    #[cfg(any(test, feature = "test-support"))]
530    pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
531        self.breakpoints.keys().cloned().collect()
532    }
533}
534
535#[derive(Clone, Copy)]
536pub enum BreakpointUpdatedReason {
537    Toggled,
538    FileSaved,
539}
540
541pub enum BreakpointStoreEvent {
542    ActiveDebugLineChanged,
543    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
544    BreakpointsCleared(Vec<Arc<Path>>),
545}
546
547impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
548
549type LogMessage = Arc<str>;
550
551#[derive(Clone, Debug)]
552pub enum BreakpointEditAction {
553    Toggle,
554    InvertState,
555    EditLogMessage(LogMessage),
556}
557
558#[derive(Clone, Debug)]
559pub enum BreakpointKind {
560    Standard,
561    Log(LogMessage),
562}
563
564impl BreakpointKind {
565    pub fn to_int(&self) -> i32 {
566        match self {
567            BreakpointKind::Standard => 0,
568            BreakpointKind::Log(_) => 1,
569        }
570    }
571
572    pub fn log_message(&self) -> Option<LogMessage> {
573        match self {
574            BreakpointKind::Standard => None,
575            BreakpointKind::Log(message) => Some(message.clone()),
576        }
577    }
578}
579
580impl PartialEq for BreakpointKind {
581    fn eq(&self, other: &Self) -> bool {
582        std::mem::discriminant(self) == std::mem::discriminant(other)
583    }
584}
585
586impl Eq for BreakpointKind {}
587
588impl Hash for BreakpointKind {
589    fn hash<H: Hasher>(&self, state: &mut H) {
590        std::mem::discriminant(self).hash(state);
591    }
592}
593
594#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
595pub enum BreakpointState {
596    Enabled,
597    Disabled,
598}
599
600impl BreakpointState {
601    #[inline]
602    pub fn is_enabled(&self) -> bool {
603        matches!(self, BreakpointState::Enabled)
604    }
605
606    #[inline]
607    pub fn is_disabled(&self) -> bool {
608        matches!(self, BreakpointState::Disabled)
609    }
610
611    #[inline]
612    pub fn to_int(&self) -> i32 {
613        match self {
614            BreakpointState::Enabled => 0,
615            BreakpointState::Disabled => 1,
616        }
617    }
618}
619
620#[derive(Clone, Debug, Hash, PartialEq, Eq)]
621pub struct Breakpoint {
622    pub kind: BreakpointKind,
623    pub state: BreakpointState,
624}
625
626impl Breakpoint {
627    pub fn new_standard() -> Self {
628        Self {
629            kind: BreakpointKind::Standard,
630            state: BreakpointState::Enabled,
631        }
632    }
633
634    pub fn new_log(log_message: &str) -> Self {
635        Self {
636            kind: BreakpointKind::Log(log_message.to_owned().into()),
637            state: BreakpointState::Enabled,
638        }
639    }
640
641    fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
642        Some(client::proto::Breakpoint {
643            position: Some(serialize_text_anchor(position)),
644            state: match self.state {
645                BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
646                BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
647            },
648            kind: match self.kind {
649                BreakpointKind::Standard => proto::BreakpointKind::Standard.into(),
650                BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(),
651            },
652            message: if let BreakpointKind::Log(message) = &self.kind {
653                Some(message.to_string())
654            } else {
655                None
656            },
657        })
658    }
659
660    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
661        Some(Self {
662            kind: match proto::BreakpointKind::from_i32(breakpoint.kind) {
663                Some(proto::BreakpointKind::Log) => {
664                    BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into())
665                }
666                None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard,
667            },
668            state: match proto::BreakpointState::from_i32(breakpoint.state) {
669                Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
670                None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
671            },
672        })
673    }
674
675    #[inline]
676    pub fn is_enabled(&self) -> bool {
677        self.state.is_enabled()
678    }
679
680    #[inline]
681    pub fn is_disabled(&self) -> bool {
682        self.state.is_disabled()
683    }
684}
685
686#[derive(Clone, Debug, Hash, PartialEq, Eq)]
687pub struct SerializedBreakpoint {
688    pub position: u32,
689    pub path: Arc<Path>,
690    pub kind: BreakpointKind,
691    pub state: BreakpointState,
692}
693
694impl From<SerializedBreakpoint> for dap::SourceBreakpoint {
695    fn from(bp: SerializedBreakpoint) -> Self {
696        Self {
697            line: bp.position as u64 + 1,
698            column: None,
699            condition: None,
700            hit_condition: None,
701            log_message: bp.kind.log_message().as_deref().map(Into::into),
702            mode: None,
703        }
704    }
705}