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