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