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::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                    log::error!("Attempted to invert a breakpoint's state that doesn't exist ");
272                }
273            }
274            BreakpointEditAction::EditLogMessage(log_message) => {
275                if !log_message.is_empty() {
276                    breakpoint.1.kind = BreakpointKind::Log(log_message.clone());
277
278                    let found_bp =
279                        breakpoint_set
280                            .breakpoints
281                            .iter_mut()
282                            .find_map(|(other_pos, other_bp)| {
283                                if breakpoint.0 == *other_pos {
284                                    Some(other_bp)
285                                } else {
286                                    None
287                                }
288                            });
289
290                    if let Some(found_bp) = found_bp {
291                        found_bp.kind = BreakpointKind::Log(log_message.clone());
292                    } else {
293                        // We did not remove any breakpoint, hence let's toggle one.
294                        breakpoint_set.breakpoints.push(breakpoint.clone());
295                    }
296                } else if matches!(&breakpoint.1.kind, BreakpointKind::Log(_)) {
297                    breakpoint_set
298                        .breakpoints
299                        .retain(|(other_pos, other_kind)| {
300                            &breakpoint.0 != other_pos
301                                && matches!(other_kind.kind, BreakpointKind::Standard)
302                        });
303                }
304            }
305        }
306
307        if breakpoint_set.breakpoints.is_empty() {
308            self.breakpoints.remove(&abs_path);
309        }
310        if let BreakpointStoreMode::Remote(remote) = &self.mode {
311            if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
312                cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
313                    project_id: remote._upstream_project_id,
314                    path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
315                    breakpoint: Some(breakpoint),
316                }))
317                .detach();
318            }
319        } else if let Some((client, project_id)) = &self.downstream_client {
320            let breakpoints = self
321                .breakpoints
322                .get(&abs_path)
323                .map(|breakpoint_set| {
324                    breakpoint_set
325                        .breakpoints
326                        .iter()
327                        .filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
328                        .collect()
329                })
330                .unwrap_or_default();
331
332            let _ = client.send(proto::BreakpointsForFile {
333                project_id: *project_id,
334                path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
335                breakpoints,
336            });
337        }
338
339        cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
340            abs_path,
341            BreakpointUpdatedReason::Toggled,
342        ));
343        cx.notify();
344    }
345
346    pub fn on_file_rename(
347        &mut self,
348        old_path: Arc<Path>,
349        new_path: Arc<Path>,
350        cx: &mut Context<Self>,
351    ) {
352        if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
353            self.breakpoints.insert(new_path.clone(), breakpoints);
354
355            cx.notify();
356        }
357    }
358
359    pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
360        let breakpoint_paths = self.breakpoints.keys().cloned().collect();
361        self.breakpoints.clear();
362        cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
363    }
364
365    pub fn breakpoints<'a>(
366        &'a self,
367        buffer: &'a Entity<Buffer>,
368        range: Option<Range<text::Anchor>>,
369        buffer_snapshot: &'a BufferSnapshot,
370        cx: &App,
371    ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
372        let abs_path = Self::abs_path_from_buffer(buffer, cx);
373        abs_path
374            .and_then(|path| self.breakpoints.get(&path))
375            .into_iter()
376            .flat_map(move |file_breakpoints| {
377                file_breakpoints.breakpoints.iter().filter({
378                    let range = range.clone();
379                    move |(position, _)| {
380                        if let Some(range) = &range {
381                            position.cmp(&range.start, buffer_snapshot).is_ge()
382                                && position.cmp(&range.end, buffer_snapshot).is_le()
383                        } else {
384                            true
385                        }
386                    }
387                })
388            })
389    }
390
391    pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, text::Anchor)> {
392        self.active_stack_frame.as_ref()
393    }
394
395    pub fn remove_active_position(
396        &mut self,
397        session_id: Option<SessionId>,
398        cx: &mut Context<Self>,
399    ) {
400        if let Some(session_id) = session_id {
401            self.active_stack_frame
402                .take_if(|(id, _, _)| *id == session_id);
403        } else {
404            self.active_stack_frame.take();
405        }
406
407        cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
408        cx.notify();
409    }
410
411    pub fn set_active_position(
412        &mut self,
413        position: (SessionId, Arc<Path>, text::Anchor),
414        cx: &mut Context<Self>,
415    ) {
416        self.active_stack_frame = Some(position);
417        cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
418        cx.notify();
419    }
420
421    pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SerializedBreakpoint> {
422        self.breakpoints
423            .get(path)
424            .map(|bp| {
425                let snapshot = bp.buffer.read(cx).snapshot();
426                bp.breakpoints
427                    .iter()
428                    .map(|(position, breakpoint)| {
429                        let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
430                        SerializedBreakpoint {
431                            position,
432                            path: path.clone(),
433                            kind: breakpoint.kind.clone(),
434                            state: breakpoint.state,
435                        }
436                    })
437                    .collect()
438            })
439            .unwrap_or_default()
440    }
441
442    pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> {
443        self.breakpoints
444            .iter()
445            .map(|(path, bp)| {
446                let snapshot = bp.buffer.read(cx).snapshot();
447                (
448                    path.clone(),
449                    bp.breakpoints
450                        .iter()
451                        .map(|(position, breakpoint)| {
452                            let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
453                            SerializedBreakpoint {
454                                position,
455                                path: path.clone(),
456                                kind: breakpoint.kind.clone(),
457                                state: breakpoint.state,
458                            }
459                        })
460                        .collect(),
461                )
462            })
463            .collect()
464    }
465
466    pub fn with_serialized_breakpoints(
467        &self,
468        breakpoints: BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
469        cx: &mut Context<'_, BreakpointStore>,
470    ) -> Task<Result<()>> {
471        if let BreakpointStoreMode::Local(mode) = &self.mode {
472            let mode = mode.clone();
473            cx.spawn(async move |this, cx| {
474                let mut new_breakpoints = BTreeMap::default();
475                for (path, bps) in breakpoints {
476                    if bps.is_empty() {
477                        continue;
478                    }
479                    let (worktree, relative_path) = mode
480                        .worktree_store
481                        .update(cx, |this, cx| {
482                            this.find_or_create_worktree(&path, false, cx)
483                        })?
484                        .await?;
485                    let buffer = mode
486                        .buffer_store
487                        .update(cx, |this, cx| {
488                            let path = ProjectPath {
489                                worktree_id: worktree.read(cx).id(),
490                                path: relative_path.into(),
491                            };
492                            this.open_buffer(path, cx)
493                        })?
494                        .await;
495                    let Ok(buffer) = buffer else {
496                        log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
497                        continue;
498                    };
499                    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
500
501                    let mut breakpoints_for_file =
502                        this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
503
504                    for bp in bps {
505                        let position = snapshot.anchor_before(PointUtf16::new(bp.position, 0));
506                        breakpoints_for_file.breakpoints.push((
507                            position,
508                            Breakpoint {
509                                kind: bp.kind,
510                                state: bp.state,
511                            },
512                        ))
513                    }
514                    new_breakpoints.insert(path, breakpoints_for_file);
515                }
516                this.update(cx, |this, cx| {
517                    this.breakpoints = new_breakpoints;
518                    cx.notify();
519                })?;
520
521                Ok(())
522            })
523        } else {
524            Task::ready(Ok(()))
525        }
526    }
527
528    #[cfg(any(test, feature = "test-support"))]
529    pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
530        self.breakpoints.keys().cloned().collect()
531    }
532}
533
534#[derive(Clone, Copy)]
535pub enum BreakpointUpdatedReason {
536    Toggled,
537    FileSaved,
538}
539
540pub enum BreakpointStoreEvent {
541    ActiveDebugLineChanged,
542    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
543    BreakpointsCleared(Vec<Arc<Path>>),
544}
545
546impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
547
548type LogMessage = Arc<str>;
549
550#[derive(Clone, Debug)]
551pub enum BreakpointEditAction {
552    Toggle,
553    InvertState,
554    EditLogMessage(LogMessage),
555}
556
557#[derive(Clone, Debug)]
558pub enum BreakpointKind {
559    Standard,
560    Log(LogMessage),
561}
562
563impl BreakpointKind {
564    pub fn to_int(&self) -> i32 {
565        match self {
566            BreakpointKind::Standard => 0,
567            BreakpointKind::Log(_) => 1,
568        }
569    }
570
571    pub fn log_message(&self) -> Option<LogMessage> {
572        match self {
573            BreakpointKind::Standard => None,
574            BreakpointKind::Log(message) => Some(message.clone()),
575        }
576    }
577}
578
579impl PartialEq for BreakpointKind {
580    fn eq(&self, other: &Self) -> bool {
581        std::mem::discriminant(self) == std::mem::discriminant(other)
582    }
583}
584
585impl Eq for BreakpointKind {}
586
587impl Hash for BreakpointKind {
588    fn hash<H: Hasher>(&self, state: &mut H) {
589        std::mem::discriminant(self).hash(state);
590    }
591}
592
593#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
594pub enum BreakpointState {
595    Enabled,
596    Disabled,
597}
598
599impl BreakpointState {
600    #[inline]
601    pub fn is_enabled(&self) -> bool {
602        matches!(self, BreakpointState::Enabled)
603    }
604
605    #[inline]
606    pub fn is_disabled(&self) -> bool {
607        matches!(self, BreakpointState::Disabled)
608    }
609
610    #[inline]
611    pub fn to_int(&self) -> i32 {
612        match self {
613            BreakpointState::Enabled => 0,
614            BreakpointState::Disabled => 1,
615        }
616    }
617}
618
619#[derive(Clone, Debug, Hash, PartialEq, Eq)]
620pub struct Breakpoint {
621    pub kind: BreakpointKind,
622    pub state: BreakpointState,
623}
624
625impl Breakpoint {
626    pub fn new_standard() -> Self {
627        Self {
628            kind: BreakpointKind::Standard,
629            state: BreakpointState::Enabled,
630        }
631    }
632
633    pub fn new_log(log_message: &str) -> Self {
634        Self {
635            kind: BreakpointKind::Log(log_message.to_owned().into()),
636            state: BreakpointState::Enabled,
637        }
638    }
639
640    fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
641        Some(client::proto::Breakpoint {
642            position: Some(serialize_text_anchor(position)),
643            state: match self.state {
644                BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
645                BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
646            },
647            kind: match self.kind {
648                BreakpointKind::Standard => proto::BreakpointKind::Standard.into(),
649                BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(),
650            },
651            message: if let BreakpointKind::Log(message) = &self.kind {
652                Some(message.to_string())
653            } else {
654                None
655            },
656        })
657    }
658
659    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
660        Some(Self {
661            kind: match proto::BreakpointKind::from_i32(breakpoint.kind) {
662                Some(proto::BreakpointKind::Log) => {
663                    BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into())
664                }
665                None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard,
666            },
667            state: match proto::BreakpointState::from_i32(breakpoint.state) {
668                Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
669                None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
670            },
671        })
672    }
673
674    #[inline]
675    pub fn is_enabled(&self) -> bool {
676        self.state.is_enabled()
677    }
678
679    #[inline]
680    pub fn is_disabled(&self) -> bool {
681        self.state.is_disabled()
682    }
683}
684
685#[derive(Clone, Debug, Hash, PartialEq, Eq)]
686pub struct SerializedBreakpoint {
687    pub position: u32,
688    pub path: Arc<Path>,
689    pub kind: BreakpointKind,
690    pub state: BreakpointState,
691}
692
693impl From<SerializedBreakpoint> for dap::SourceBreakpoint {
694    fn from(bp: SerializedBreakpoint) -> Self {
695        Self {
696            line: bp.position as u64 + 1,
697            column: None,
698            condition: None,
699            hit_condition: None,
700            log_message: bp.kind.log_message().as_deref().map(Into::into),
701            mode: None,
702        }
703    }
704}