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, 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<gpui::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 breakpoints<'a>(
345        &'a self,
346        buffer: &'a Entity<Buffer>,
347        range: Option<Range<text::Anchor>>,
348        buffer_snapshot: BufferSnapshot,
349        cx: &App,
350    ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
351        let abs_path = Self::abs_path_from_buffer(buffer, cx);
352        abs_path
353            .and_then(|path| self.breakpoints.get(&path))
354            .into_iter()
355            .flat_map(move |file_breakpoints| {
356                file_breakpoints.breakpoints.iter().filter({
357                    let range = range.clone();
358                    let buffer_snapshot = buffer_snapshot.clone();
359                    move |(position, _)| {
360                        if let Some(range) = &range {
361                            position.cmp(&range.start, &buffer_snapshot).is_ge()
362                                && position.cmp(&range.end, &buffer_snapshot).is_le()
363                        } else {
364                            true
365                        }
366                    }
367                })
368            })
369    }
370
371    pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, text::Anchor)> {
372        self.active_stack_frame.as_ref()
373    }
374
375    pub fn remove_active_position(
376        &mut self,
377        session_id: Option<SessionId>,
378        cx: &mut Context<Self>,
379    ) {
380        if let Some(session_id) = session_id {
381            self.active_stack_frame
382                .take_if(|(id, _, _)| *id == session_id);
383        } else {
384            self.active_stack_frame.take();
385        }
386
387        cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
388        cx.notify();
389    }
390
391    pub fn set_active_position(
392        &mut self,
393        position: (SessionId, Arc<Path>, text::Anchor),
394        cx: &mut Context<Self>,
395    ) {
396        self.active_stack_frame = Some(position);
397        cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
398        cx.notify();
399    }
400
401    pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SerializedBreakpoint> {
402        self.breakpoints
403            .get(path)
404            .map(|bp| {
405                let snapshot = bp.buffer.read(cx).snapshot();
406                bp.breakpoints
407                    .iter()
408                    .map(|(position, breakpoint)| {
409                        let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
410                        SerializedBreakpoint {
411                            position,
412                            path: path.clone(),
413                            kind: breakpoint.kind.clone(),
414                        }
415                    })
416                    .collect()
417            })
418            .unwrap_or_default()
419    }
420
421    pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>> {
422        self.breakpoints
423            .iter()
424            .map(|(path, bp)| {
425                let snapshot = bp.buffer.read(cx).snapshot();
426                (
427                    path.clone(),
428                    bp.breakpoints
429                        .iter()
430                        .map(|(position, breakpoint)| {
431                            let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
432                            SerializedBreakpoint {
433                                position,
434                                path: path.clone(),
435                                kind: breakpoint.kind.clone(),
436                            }
437                        })
438                        .collect(),
439                )
440            })
441            .collect()
442    }
443
444    pub fn with_serialized_breakpoints(
445        &self,
446        breakpoints: BTreeMap<Arc<Path>, Vec<SerializedBreakpoint>>,
447        cx: &mut Context<'_, BreakpointStore>,
448    ) -> Task<Result<()>> {
449        if let BreakpointStoreMode::Local(mode) = &self.mode {
450            let mode = mode.clone();
451            cx.spawn(move |this, mut cx| async move {
452                let mut new_breakpoints = BTreeMap::default();
453                for (path, bps) in breakpoints {
454                    if bps.is_empty() {
455                        continue;
456                    }
457                    let (worktree, relative_path) = mode
458                        .worktree_store
459                        .update(&mut cx, |this, cx| {
460                            this.find_or_create_worktree(&path, false, cx)
461                        })?
462                        .await?;
463                    let buffer = mode
464                        .buffer_store
465                        .update(&mut cx, |this, cx| {
466                            let path = ProjectPath {
467                                worktree_id: worktree.read(cx).id(),
468                                path: relative_path.into(),
469                            };
470                            this.open_buffer(path, cx)
471                        })?
472                        .await;
473                    let Ok(buffer) = buffer else {
474                        log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
475                        continue;
476                    };
477                    let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
478
479                    let mut breakpoints_for_file =
480                        this.update(&mut cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
481
482                    for bp in bps {
483                        let position = snapshot.anchor_before(PointUtf16::new(bp.position, 0));
484                        breakpoints_for_file
485                            .breakpoints
486                            .push((position, Breakpoint { kind: bp.kind }))
487                    }
488                    new_breakpoints.insert(path, breakpoints_for_file);
489                }
490                this.update(&mut cx, |this, cx| {
491                    this.breakpoints = new_breakpoints;
492                    cx.notify();
493                })?;
494
495                Ok(())
496            })
497        } else {
498            Task::ready(Ok(()))
499        }
500    }
501}
502
503#[derive(Clone, Copy)]
504pub enum BreakpointUpdatedReason {
505    Toggled,
506    FileSaved,
507}
508
509pub enum BreakpointStoreEvent {
510    ActiveDebugLineChanged,
511    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
512}
513
514impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
515
516type LogMessage = Arc<str>;
517
518#[derive(Clone, Debug)]
519pub enum BreakpointEditAction {
520    Toggle,
521    EditLogMessage(LogMessage),
522}
523
524#[derive(Clone, Debug)]
525pub enum BreakpointKind {
526    Standard,
527    Log(LogMessage),
528}
529
530impl BreakpointKind {
531    pub fn to_int(&self) -> i32 {
532        match self {
533            BreakpointKind::Standard => 0,
534            BreakpointKind::Log(_) => 1,
535        }
536    }
537
538    pub fn log_message(&self) -> Option<LogMessage> {
539        match self {
540            BreakpointKind::Standard => None,
541            BreakpointKind::Log(message) => Some(message.clone()),
542        }
543    }
544}
545
546impl PartialEq for BreakpointKind {
547    fn eq(&self, other: &Self) -> bool {
548        std::mem::discriminant(self) == std::mem::discriminant(other)
549    }
550}
551
552impl Eq for BreakpointKind {}
553
554impl Hash for BreakpointKind {
555    fn hash<H: Hasher>(&self, state: &mut H) {
556        std::mem::discriminant(self).hash(state);
557    }
558}
559
560#[derive(Clone, Debug, Hash, PartialEq, Eq)]
561pub struct Breakpoint {
562    pub kind: BreakpointKind,
563}
564
565impl Breakpoint {
566    fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
567        Some(client::proto::Breakpoint {
568            position: Some(serialize_text_anchor(position)),
569
570            kind: match self.kind {
571                BreakpointKind::Standard => proto::BreakpointKind::Standard.into(),
572                BreakpointKind::Log(_) => proto::BreakpointKind::Log.into(),
573            },
574            message: if let BreakpointKind::Log(message) = &self.kind {
575                Some(message.to_string())
576            } else {
577                None
578            },
579        })
580    }
581
582    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
583        Some(Self {
584            kind: match proto::BreakpointKind::from_i32(breakpoint.kind) {
585                Some(proto::BreakpointKind::Log) => {
586                    BreakpointKind::Log(breakpoint.message.clone().unwrap_or_default().into())
587                }
588                None | Some(proto::BreakpointKind::Standard) => BreakpointKind::Standard,
589            },
590        })
591    }
592}
593
594#[derive(Clone, Debug, Hash, PartialEq, Eq)]
595pub struct SerializedBreakpoint {
596    pub position: u32,
597    pub path: Arc<Path>,
598    pub kind: BreakpointKind,
599}
600
601impl From<SerializedBreakpoint> for dap::SourceBreakpoint {
602    fn from(bp: SerializedBreakpoint) -> Self {
603        Self {
604            line: bp.position as u64 + 1,
605            column: None,
606            condition: None,
607            hit_condition: None,
608            log_message: bp.kind.log_message().as_deref().map(Into::into),
609            mode: None,
610        }
611    }
612}