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