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