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