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)]
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::ActiveDebugLineChanged);
525        cx.notify();
526    }
527
528    pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context<Self>) {
529        self.active_stack_frame = Some(position);
530        cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
531        cx.notify();
532    }
533
534    pub fn breakpoint_at_row(
535        &self,
536        path: &Path,
537        row: u32,
538        cx: &App,
539    ) -> Option<(Entity<Buffer>, (text::Anchor, Breakpoint))> {
540        self.breakpoints.get(path).and_then(|breakpoints| {
541            let snapshot = breakpoints.buffer.read(cx).text_snapshot();
542
543            breakpoints
544                .breakpoints
545                .iter()
546                .find(|(anchor, _)| anchor.summary::<Point>(&snapshot).row == row)
547                .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone()))
548        })
549    }
550
551    pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
552        self.breakpoints
553            .get(path)
554            .map(|bp| {
555                let snapshot = bp.buffer.read(cx).snapshot();
556                bp.breakpoints
557                    .iter()
558                    .map(|(position, breakpoint)| {
559                        let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
560                        SourceBreakpoint {
561                            row: position,
562                            path: path.clone(),
563                            state: breakpoint.state,
564                            message: breakpoint.message.clone(),
565                            condition: breakpoint.condition.clone(),
566                            hit_condition: breakpoint.hit_condition.clone(),
567                        }
568                    })
569                    .collect()
570            })
571            .unwrap_or_default()
572    }
573
574    pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
575        self.breakpoints
576            .iter()
577            .map(|(path, bp)| {
578                let snapshot = bp.buffer.read(cx).snapshot();
579                (
580                    path.clone(),
581                    bp.breakpoints
582                        .iter()
583                        .map(|(position, breakpoint)| {
584                            let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
585                            SourceBreakpoint {
586                                row: position,
587                                path: path.clone(),
588                                message: breakpoint.message.clone(),
589                                state: breakpoint.state,
590                                hit_condition: breakpoint.hit_condition.clone(),
591                                condition: breakpoint.condition.clone(),
592                            }
593                        })
594                        .collect(),
595                )
596            })
597            .collect()
598    }
599
600    pub fn with_serialized_breakpoints(
601        &self,
602        breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
603        cx: &mut Context<BreakpointStore>,
604    ) -> Task<Result<()>> {
605        if let BreakpointStoreMode::Local(mode) = &self.mode {
606            let mode = mode.clone();
607            cx.spawn(async move |this, cx| {
608                let mut new_breakpoints = BTreeMap::default();
609                for (path, bps) in breakpoints {
610                    if bps.is_empty() {
611                        continue;
612                    }
613                    let (worktree, relative_path) = mode
614                        .worktree_store
615                        .update(cx, |this, cx| {
616                            this.find_or_create_worktree(&path, false, cx)
617                        })?
618                        .await?;
619                    let buffer = mode
620                        .buffer_store
621                        .update(cx, |this, cx| {
622                            let path = ProjectPath {
623                                worktree_id: worktree.read(cx).id(),
624                                path: relative_path.into(),
625                            };
626                            this.open_buffer(path, cx)
627                        })?
628                        .await;
629                    let Ok(buffer) = buffer else {
630                        log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
631                        continue;
632                    };
633                    let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
634
635                    let mut breakpoints_for_file =
636                        this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
637
638                    for bp in bps {
639                        let max_point = snapshot.max_point_utf16();
640                        let point = PointUtf16::new(bp.row, 0);
641                        if point > max_point {
642                            log::error!("skipping a deserialized breakpoint that's out of range");
643                            continue;
644                        }
645                        let position = snapshot.anchor_after(point);
646                        breakpoints_for_file.breakpoints.push((
647                            position,
648                            Breakpoint {
649                                message: bp.message,
650                                state: bp.state,
651                                condition: bp.condition,
652                                hit_condition: bp.hit_condition,
653                            },
654                        ))
655                    }
656                    new_breakpoints.insert(path, breakpoints_for_file);
657                }
658                this.update(cx, |this, cx| {
659                    log::info!("Finish deserializing breakpoints & initializing breakpoint store");
660                    for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
661                        (path.to_string_lossy(), bp_in_file.breakpoints.len())
662                    }) {
663                        let breakpoint_str = if count > 1 {
664                            "breakpoints"
665                        } else {
666                            "breakpoint"
667                        };
668                        log::info!("Deserialized {count} {breakpoint_str} at path: {path}");
669                    }
670
671                    this.breakpoints = new_breakpoints;
672
673                    cx.notify();
674                })?;
675
676                Ok(())
677            })
678        } else {
679            Task::ready(Ok(()))
680        }
681    }
682
683    #[cfg(any(test, feature = "test-support"))]
684    pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
685        self.breakpoints.keys().cloned().collect()
686    }
687}
688
689#[derive(Clone, Copy)]
690pub enum BreakpointUpdatedReason {
691    Toggled,
692    FileSaved,
693}
694
695pub enum BreakpointStoreEvent {
696    ActiveDebugLineChanged,
697    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
698    BreakpointsCleared(Vec<Arc<Path>>),
699}
700
701impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
702
703type BreakpointMessage = Arc<str>;
704
705#[derive(Clone, Debug)]
706pub enum BreakpointEditAction {
707    Toggle,
708    InvertState,
709    EditLogMessage(BreakpointMessage),
710    EditCondition(BreakpointMessage),
711    EditHitCondition(BreakpointMessage),
712}
713
714#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
715pub enum BreakpointState {
716    Enabled,
717    Disabled,
718}
719
720impl BreakpointState {
721    #[inline]
722    pub fn is_enabled(&self) -> bool {
723        matches!(self, BreakpointState::Enabled)
724    }
725
726    #[inline]
727    pub fn is_disabled(&self) -> bool {
728        matches!(self, BreakpointState::Disabled)
729    }
730
731    #[inline]
732    pub fn to_int(&self) -> i32 {
733        match self {
734            BreakpointState::Enabled => 0,
735            BreakpointState::Disabled => 1,
736        }
737    }
738}
739
740#[derive(Clone, Debug, Hash, PartialEq, Eq)]
741pub struct Breakpoint {
742    pub message: Option<BreakpointMessage>,
743    /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
744    pub hit_condition: Option<BreakpointMessage>,
745    pub condition: Option<BreakpointMessage>,
746    pub state: BreakpointState,
747}
748
749impl Breakpoint {
750    pub fn new_standard() -> Self {
751        Self {
752            state: BreakpointState::Enabled,
753            hit_condition: None,
754            condition: None,
755            message: None,
756        }
757    }
758
759    pub fn new_condition(hit_condition: &str) -> Self {
760        Self {
761            state: BreakpointState::Enabled,
762            condition: None,
763            hit_condition: Some(hit_condition.into()),
764            message: None,
765        }
766    }
767
768    pub fn new_log(log_message: &str) -> Self {
769        Self {
770            state: BreakpointState::Enabled,
771            hit_condition: None,
772            condition: None,
773            message: Some(log_message.into()),
774        }
775    }
776
777    fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
778        Some(client::proto::Breakpoint {
779            position: Some(serialize_text_anchor(position)),
780            state: match self.state {
781                BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
782                BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
783            },
784            message: self.message.as_ref().map(|s| String::from(s.as_ref())),
785            condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
786            hit_condition: self
787                .hit_condition
788                .as_ref()
789                .map(|s| String::from(s.as_ref())),
790        })
791    }
792
793    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
794        Some(Self {
795            state: match proto::BreakpointState::from_i32(breakpoint.state) {
796                Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
797                None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
798            },
799            message: breakpoint.message.map(Into::into),
800            condition: breakpoint.condition.map(Into::into),
801            hit_condition: breakpoint.hit_condition.map(Into::into),
802        })
803    }
804
805    #[inline]
806    pub fn is_enabled(&self) -> bool {
807        self.state.is_enabled()
808    }
809
810    #[inline]
811    pub fn is_disabled(&self) -> bool {
812        self.state.is_disabled()
813    }
814}
815
816/// Breakpoint for location within source code.
817#[derive(Clone, Debug, Hash, PartialEq, Eq)]
818pub struct SourceBreakpoint {
819    pub row: u32,
820    pub path: Arc<Path>,
821    pub message: Option<Arc<str>>,
822    pub condition: Option<Arc<str>>,
823    pub hit_condition: Option<Arc<str>>,
824    pub state: BreakpointState,
825}
826
827impl From<SourceBreakpoint> for dap::SourceBreakpoint {
828    fn from(bp: SourceBreakpoint) -> Self {
829        Self {
830            line: bp.row as u64 + 1,
831            column: None,
832            condition: bp
833                .condition
834                .map(|condition| String::from(condition.as_ref())),
835            hit_condition: bp
836                .hit_condition
837                .map(|hit_condition| String::from(hit_condition.as_ref())),
838            log_message: bp.message.map(|message| String::from(message.as_ref())),
839            mode: None,
840        }
841    }
842}