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    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                    this.breakpoints = new_breakpoints;
570                    cx.notify();
571                })?;
572
573                Ok(())
574            })
575        } else {
576            Task::ready(Ok(()))
577        }
578    }
579
580    #[cfg(any(test, feature = "test-support"))]
581    pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
582        self.breakpoints.keys().cloned().collect()
583    }
584}
585
586#[derive(Clone, Copy)]
587pub enum BreakpointUpdatedReason {
588    Toggled,
589    FileSaved,
590}
591
592pub enum BreakpointStoreEvent {
593    ActiveDebugLineChanged,
594    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
595    BreakpointsCleared(Vec<Arc<Path>>),
596}
597
598impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
599
600type BreakpointMessage = Arc<str>;
601
602#[derive(Clone, Debug)]
603pub enum BreakpointEditAction {
604    Toggle,
605    InvertState,
606    EditLogMessage(BreakpointMessage),
607    EditCondition(BreakpointMessage),
608    EditHitCondition(BreakpointMessage),
609}
610
611#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
612pub enum BreakpointState {
613    Enabled,
614    Disabled,
615}
616
617impl BreakpointState {
618    #[inline]
619    pub fn is_enabled(&self) -> bool {
620        matches!(self, BreakpointState::Enabled)
621    }
622
623    #[inline]
624    pub fn is_disabled(&self) -> bool {
625        matches!(self, BreakpointState::Disabled)
626    }
627
628    #[inline]
629    pub fn to_int(&self) -> i32 {
630        match self {
631            BreakpointState::Enabled => 0,
632            BreakpointState::Disabled => 1,
633        }
634    }
635}
636
637#[derive(Clone, Debug, Hash, PartialEq, Eq)]
638pub struct Breakpoint {
639    pub message: Option<BreakpointMessage>,
640    /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
641    pub hit_condition: Option<BreakpointMessage>,
642    pub condition: Option<BreakpointMessage>,
643    pub state: BreakpointState,
644}
645
646impl Breakpoint {
647    pub fn new_standard() -> Self {
648        Self {
649            state: BreakpointState::Enabled,
650            hit_condition: None,
651            condition: None,
652            message: None,
653        }
654    }
655
656    pub fn new_condition(hit_condition: &str) -> Self {
657        Self {
658            state: BreakpointState::Enabled,
659            condition: None,
660            hit_condition: Some(hit_condition.into()),
661            message: None,
662        }
663    }
664
665    pub fn new_log(log_message: &str) -> Self {
666        Self {
667            state: BreakpointState::Enabled,
668            hit_condition: None,
669            condition: None,
670            message: Some(log_message.into()),
671        }
672    }
673
674    fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
675        Some(client::proto::Breakpoint {
676            position: Some(serialize_text_anchor(position)),
677            state: match self.state {
678                BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
679                BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
680            },
681            message: self.message.as_ref().map(|s| String::from(s.as_ref())),
682            condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
683            hit_condition: self
684                .hit_condition
685                .as_ref()
686                .map(|s| String::from(s.as_ref())),
687        })
688    }
689
690    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
691        Some(Self {
692            state: match proto::BreakpointState::from_i32(breakpoint.state) {
693                Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
694                None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
695            },
696            message: breakpoint.message.map(Into::into),
697            condition: breakpoint.condition.map(Into::into),
698            hit_condition: breakpoint.hit_condition.map(Into::into),
699        })
700    }
701
702    #[inline]
703    pub fn is_enabled(&self) -> bool {
704        self.state.is_enabled()
705    }
706
707    #[inline]
708    pub fn is_disabled(&self) -> bool {
709        self.state.is_disabled()
710    }
711}
712
713/// Breakpoint for location within source code.
714#[derive(Clone, Debug, Hash, PartialEq, Eq)]
715pub struct SourceBreakpoint {
716    pub row: u32,
717    pub path: Arc<Path>,
718    pub message: Option<Arc<str>>,
719    pub condition: Option<Arc<str>>,
720    pub hit_condition: Option<Arc<str>>,
721    pub state: BreakpointState,
722}
723
724impl From<SourceBreakpoint> for dap::SourceBreakpoint {
725    fn from(bp: SourceBreakpoint) -> Self {
726        Self {
727            line: bp.row as u64 + 1,
728            column: None,
729            condition: bp
730                .condition
731                .map(|condition| String::from(condition.as_ref())),
732            hit_condition: bp
733                .hit_condition
734                .map(|hit_condition| String::from(hit_condition.as_ref())),
735            log_message: bp.message.map(|message| String::from(message.as_ref())),
736            mode: None,
737        }
738    }
739}