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 position = snapshot.anchor_after(PointUtf16::new(bp.row, 0));
595                        breakpoints_for_file.breakpoints.push((
596                            position,
597                            Breakpoint {
598                                message: bp.message,
599                                state: bp.state,
600                                condition: bp.condition,
601                                hit_condition: bp.hit_condition,
602                            },
603                        ))
604                    }
605                    new_breakpoints.insert(path, breakpoints_for_file);
606                }
607                this.update(cx, |this, cx| {
608                    log::info!("Finish deserializing breakpoints & initializing breakpoint store");
609                    for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
610                        (path.to_string_lossy(), bp_in_file.breakpoints.len())
611                    }) {
612                        let breakpoint_str = if count > 1 {
613                            "breakpoints"
614                        } else {
615                            "breakpoint"
616                        };
617                        log::info!("Deserialized {count} {breakpoint_str} at path: {path}");
618                    }
619
620                    this.breakpoints = new_breakpoints;
621
622                    cx.notify();
623                })?;
624
625                Ok(())
626            })
627        } else {
628            Task::ready(Ok(()))
629        }
630    }
631
632    #[cfg(any(test, feature = "test-support"))]
633    pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
634        self.breakpoints.keys().cloned().collect()
635    }
636}
637
638#[derive(Clone, Copy)]
639pub enum BreakpointUpdatedReason {
640    Toggled,
641    FileSaved,
642}
643
644pub enum BreakpointStoreEvent {
645    ActiveDebugLineChanged,
646    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
647    BreakpointsCleared(Vec<Arc<Path>>),
648}
649
650impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
651
652type BreakpointMessage = Arc<str>;
653
654#[derive(Clone, Debug)]
655pub enum BreakpointEditAction {
656    Toggle,
657    InvertState,
658    EditLogMessage(BreakpointMessage),
659    EditCondition(BreakpointMessage),
660    EditHitCondition(BreakpointMessage),
661}
662
663#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
664pub enum BreakpointState {
665    Enabled,
666    Disabled,
667}
668
669impl BreakpointState {
670    #[inline]
671    pub fn is_enabled(&self) -> bool {
672        matches!(self, BreakpointState::Enabled)
673    }
674
675    #[inline]
676    pub fn is_disabled(&self) -> bool {
677        matches!(self, BreakpointState::Disabled)
678    }
679
680    #[inline]
681    pub fn to_int(&self) -> i32 {
682        match self {
683            BreakpointState::Enabled => 0,
684            BreakpointState::Disabled => 1,
685        }
686    }
687}
688
689#[derive(Clone, Debug, Hash, PartialEq, Eq)]
690pub struct Breakpoint {
691    pub message: Option<BreakpointMessage>,
692    /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
693    pub hit_condition: Option<BreakpointMessage>,
694    pub condition: Option<BreakpointMessage>,
695    pub state: BreakpointState,
696}
697
698impl Breakpoint {
699    pub fn new_standard() -> Self {
700        Self {
701            state: BreakpointState::Enabled,
702            hit_condition: None,
703            condition: None,
704            message: None,
705        }
706    }
707
708    pub fn new_condition(hit_condition: &str) -> Self {
709        Self {
710            state: BreakpointState::Enabled,
711            condition: None,
712            hit_condition: Some(hit_condition.into()),
713            message: None,
714        }
715    }
716
717    pub fn new_log(log_message: &str) -> Self {
718        Self {
719            state: BreakpointState::Enabled,
720            hit_condition: None,
721            condition: None,
722            message: Some(log_message.into()),
723        }
724    }
725
726    fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
727        Some(client::proto::Breakpoint {
728            position: Some(serialize_text_anchor(position)),
729            state: match self.state {
730                BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
731                BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
732            },
733            message: self.message.as_ref().map(|s| String::from(s.as_ref())),
734            condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
735            hit_condition: self
736                .hit_condition
737                .as_ref()
738                .map(|s| String::from(s.as_ref())),
739        })
740    }
741
742    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
743        Some(Self {
744            state: match proto::BreakpointState::from_i32(breakpoint.state) {
745                Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
746                None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
747            },
748            message: breakpoint.message.map(Into::into),
749            condition: breakpoint.condition.map(Into::into),
750            hit_condition: breakpoint.hit_condition.map(Into::into),
751        })
752    }
753
754    #[inline]
755    pub fn is_enabled(&self) -> bool {
756        self.state.is_enabled()
757    }
758
759    #[inline]
760    pub fn is_disabled(&self) -> bool {
761        self.state.is_disabled()
762    }
763}
764
765/// Breakpoint for location within source code.
766#[derive(Clone, Debug, Hash, PartialEq, Eq)]
767pub struct SourceBreakpoint {
768    pub row: u32,
769    pub path: Arc<Path>,
770    pub message: Option<Arc<str>>,
771    pub condition: Option<Arc<str>>,
772    pub hit_condition: Option<Arc<str>>,
773    pub state: BreakpointState,
774}
775
776impl From<SourceBreakpoint> for dap::SourceBreakpoint {
777    fn from(bp: SourceBreakpoint) -> Self {
778        Self {
779            line: bp.row as u64 + 1,
780            column: None,
781            condition: bp
782                .condition
783                .map(|condition| String::from(condition.as_ref())),
784            hit_condition: bp
785                .hit_condition
786                .map(|hit_condition| String::from(hit_condition.as_ref())),
787            log_message: bp.message.map(|message| String::from(message.as_ref())),
788            mode: None,
789        }
790    }
791}