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