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