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