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::{Point, 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 breakpoint_at_row(
468        &self,
469        path: &Path,
470        row: u32,
471        cx: &App,
472    ) -> Option<(Entity<Buffer>, (text::Anchor, Breakpoint))> {
473        self.breakpoints.get(path).and_then(|breakpoints| {
474            let snapshot = breakpoints.buffer.read(cx).text_snapshot();
475
476            breakpoints
477                .breakpoints
478                .iter()
479                .find(|(anchor, _)| anchor.summary::<Point>(&snapshot).row == row)
480                .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone()))
481        })
482    }
483
484    pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
485        self.breakpoints
486            .get(path)
487            .map(|bp| {
488                let snapshot = bp.buffer.read(cx).snapshot();
489                bp.breakpoints
490                    .iter()
491                    .map(|(position, breakpoint)| {
492                        let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
493                        SourceBreakpoint {
494                            row: position,
495                            path: path.clone(),
496                            state: breakpoint.state,
497                            message: breakpoint.message.clone(),
498                            condition: breakpoint.condition.clone(),
499                            hit_condition: breakpoint.hit_condition.clone(),
500                        }
501                    })
502                    .collect()
503            })
504            .unwrap_or_default()
505    }
506
507    pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
508        self.breakpoints
509            .iter()
510            .map(|(path, bp)| {
511                let snapshot = bp.buffer.read(cx).snapshot();
512                (
513                    path.clone(),
514                    bp.breakpoints
515                        .iter()
516                        .map(|(position, breakpoint)| {
517                            let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
518                            SourceBreakpoint {
519                                row: position,
520                                path: path.clone(),
521                                message: breakpoint.message.clone(),
522                                state: breakpoint.state,
523                                hit_condition: breakpoint.hit_condition.clone(),
524                                condition: breakpoint.condition.clone(),
525                            }
526                        })
527                        .collect(),
528                )
529            })
530            .collect()
531    }
532
533    pub fn with_serialized_breakpoints(
534        &self,
535        breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
536        cx: &mut Context<BreakpointStore>,
537    ) -> Task<Result<()>> {
538        if let BreakpointStoreMode::Local(mode) = &self.mode {
539            let mode = mode.clone();
540            cx.spawn(async move |this, cx| {
541                let mut new_breakpoints = BTreeMap::default();
542                for (path, bps) in breakpoints {
543                    if bps.is_empty() {
544                        continue;
545                    }
546                    let (worktree, relative_path) = mode
547                        .worktree_store
548                        .update(cx, |this, cx| {
549                            this.find_or_create_worktree(&path, false, cx)
550                        })?
551                        .await?;
552                    let buffer = mode
553                        .buffer_store
554                        .update(cx, |this, cx| {
555                            let path = ProjectPath {
556                                worktree_id: worktree.read(cx).id(),
557                                path: relative_path.into(),
558                            };
559                            this.open_buffer(path, cx)
560                        })?
561                        .await;
562                    let Ok(buffer) = buffer else {
563                        log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
564                        continue;
565                    };
566                    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
567
568                    let mut breakpoints_for_file =
569                        this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
570
571                    for bp in bps {
572                        let position = snapshot.anchor_after(PointUtf16::new(bp.row, 0));
573                        breakpoints_for_file.breakpoints.push((
574                            position,
575                            Breakpoint {
576                                message: bp.message,
577                                state: bp.state,
578                                condition: bp.condition,
579                                hit_condition: bp.hit_condition,
580                            },
581                        ))
582                    }
583                    new_breakpoints.insert(path, breakpoints_for_file);
584                }
585                this.update(cx, |this, cx| {
586                    log::info!("Finish deserializing breakpoints & initializing breakpoint store");
587                    for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
588                        (path.to_string_lossy(), bp_in_file.breakpoints.len())
589                    }) {
590                        let breakpoint_str = if count > 1 {
591                            "breakpoints"
592                        } else {
593                            "breakpoint"
594                        };
595                        log::info!("Deserialized {count} {breakpoint_str} at path: {path}");
596                    }
597
598                    this.breakpoints = new_breakpoints;
599
600                    cx.notify();
601                })?;
602
603                Ok(())
604            })
605        } else {
606            Task::ready(Ok(()))
607        }
608    }
609
610    #[cfg(any(test, feature = "test-support"))]
611    pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
612        self.breakpoints.keys().cloned().collect()
613    }
614}
615
616#[derive(Clone, Copy)]
617pub enum BreakpointUpdatedReason {
618    Toggled,
619    FileSaved,
620}
621
622pub enum BreakpointStoreEvent {
623    ActiveDebugLineChanged,
624    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
625    BreakpointsCleared(Vec<Arc<Path>>),
626}
627
628impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
629
630type BreakpointMessage = Arc<str>;
631
632#[derive(Clone, Debug)]
633pub enum BreakpointEditAction {
634    Toggle,
635    InvertState,
636    EditLogMessage(BreakpointMessage),
637    EditCondition(BreakpointMessage),
638    EditHitCondition(BreakpointMessage),
639}
640
641#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
642pub enum BreakpointState {
643    Enabled,
644    Disabled,
645}
646
647impl BreakpointState {
648    #[inline]
649    pub fn is_enabled(&self) -> bool {
650        matches!(self, BreakpointState::Enabled)
651    }
652
653    #[inline]
654    pub fn is_disabled(&self) -> bool {
655        matches!(self, BreakpointState::Disabled)
656    }
657
658    #[inline]
659    pub fn to_int(&self) -> i32 {
660        match self {
661            BreakpointState::Enabled => 0,
662            BreakpointState::Disabled => 1,
663        }
664    }
665}
666
667#[derive(Clone, Debug, Hash, PartialEq, Eq)]
668pub struct Breakpoint {
669    pub message: Option<BreakpointMessage>,
670    /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
671    pub hit_condition: Option<BreakpointMessage>,
672    pub condition: Option<BreakpointMessage>,
673    pub state: BreakpointState,
674}
675
676impl Breakpoint {
677    pub fn new_standard() -> Self {
678        Self {
679            state: BreakpointState::Enabled,
680            hit_condition: None,
681            condition: None,
682            message: None,
683        }
684    }
685
686    pub fn new_condition(hit_condition: &str) -> Self {
687        Self {
688            state: BreakpointState::Enabled,
689            condition: None,
690            hit_condition: Some(hit_condition.into()),
691            message: None,
692        }
693    }
694
695    pub fn new_log(log_message: &str) -> Self {
696        Self {
697            state: BreakpointState::Enabled,
698            hit_condition: None,
699            condition: None,
700            message: Some(log_message.into()),
701        }
702    }
703
704    fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
705        Some(client::proto::Breakpoint {
706            position: Some(serialize_text_anchor(position)),
707            state: match self.state {
708                BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
709                BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
710            },
711            message: self.message.as_ref().map(|s| String::from(s.as_ref())),
712            condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
713            hit_condition: self
714                .hit_condition
715                .as_ref()
716                .map(|s| String::from(s.as_ref())),
717        })
718    }
719
720    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
721        Some(Self {
722            state: match proto::BreakpointState::from_i32(breakpoint.state) {
723                Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
724                None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
725            },
726            message: breakpoint.message.map(Into::into),
727            condition: breakpoint.condition.map(Into::into),
728            hit_condition: breakpoint.hit_condition.map(Into::into),
729        })
730    }
731
732    #[inline]
733    pub fn is_enabled(&self) -> bool {
734        self.state.is_enabled()
735    }
736
737    #[inline]
738    pub fn is_disabled(&self) -> bool {
739        self.state.is_disabled()
740    }
741}
742
743/// Breakpoint for location within source code.
744#[derive(Clone, Debug, Hash, PartialEq, Eq)]
745pub struct SourceBreakpoint {
746    pub row: u32,
747    pub path: Arc<Path>,
748    pub message: Option<Arc<str>>,
749    pub condition: Option<Arc<str>>,
750    pub hit_condition: Option<Arc<str>>,
751    pub state: BreakpointState,
752}
753
754impl From<SourceBreakpoint> for dap::SourceBreakpoint {
755    fn from(bp: SourceBreakpoint) -> Self {
756        Self {
757            line: bp.row as u64 + 1,
758            column: None,
759            condition: bp
760                .condition
761                .map(|condition| String::from(condition.as_ref())),
762            hit_condition: bp
763                .hit_condition
764                .map(|hit_condition| String::from(hit_condition.as_ref())),
765            log_message: bp.message.map(|message| String::from(message.as_ref())),
766            mode: None,
767        }
768    }
769}