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