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.clone(), 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(&mut 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            .and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
 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.clone());
 454                    } else {
 455                        breakpoint.bp.message = Some(log_message.clone());
 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.clone());
 486                    } else {
 487                        breakpoint.bp.hit_condition = Some(hit_condition.clone());
 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.clone());
 518                    } else {
 519                        breakpoint.bp.condition = Some(condition.clone());
 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.clone(), 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                            if 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                        }
 633                        let session_state = active_session_id
 634                            .and_then(|id| bp.session_state.get(&id))
 635                            .copied();
 636                        Some((&bp.bp, session_state))
 637                    }
 638                })
 639            })
 640    }
 641
 642    pub fn active_position(&self) -> Option<&ActiveStackFrame> {
 643        self.active_stack_frame.as_ref()
 644    }
 645
 646    pub fn remove_active_position(
 647        &mut self,
 648        session_id: Option<SessionId>,
 649        cx: &mut Context<Self>,
 650    ) {
 651        if let Some(session_id) = session_id {
 652            self.active_stack_frame
 653                .take_if(|active_stack_frame| active_stack_frame.session_id == session_id);
 654        } else {
 655            self.active_stack_frame.take();
 656        }
 657
 658        cx.emit(BreakpointStoreEvent::ClearDebugLines);
 659        cx.notify();
 660    }
 661
 662    pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context<Self>) {
 663        if self
 664            .active_stack_frame
 665            .as_ref()
 666            .is_some_and(|active_position| active_position == &position)
 667        {
 668            cx.emit(BreakpointStoreEvent::SetDebugLine);
 669            return;
 670        }
 671
 672        if self.active_stack_frame.is_some() {
 673            cx.emit(BreakpointStoreEvent::ClearDebugLines);
 674        }
 675
 676        self.active_stack_frame = Some(position);
 677
 678        cx.emit(BreakpointStoreEvent::SetDebugLine);
 679        cx.notify();
 680    }
 681
 682    pub fn breakpoint_at_row(
 683        &self,
 684        path: &Path,
 685        row: u32,
 686        cx: &App,
 687    ) -> Option<(Entity<Buffer>, BreakpointWithPosition)> {
 688        self.breakpoints.get(path).and_then(|breakpoints| {
 689            let snapshot = breakpoints.buffer.read(cx).text_snapshot();
 690
 691            breakpoints
 692                .breakpoints
 693                .iter()
 694                .find(|bp| bp.position().summary::<Point>(&snapshot).row == row)
 695                .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone()))
 696        })
 697    }
 698
 699    pub fn breakpoints_from_path(&self, path: &Arc<Path>) -> Vec<BreakpointWithPosition> {
 700        self.breakpoints
 701            .get(path)
 702            .map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect())
 703            .unwrap_or_default()
 704    }
 705
 706    pub fn source_breakpoints_from_path(
 707        &self,
 708        path: &Arc<Path>,
 709        cx: &App,
 710    ) -> Vec<SourceBreakpoint> {
 711        self.breakpoints
 712            .get(path)
 713            .map(|bp| {
 714                let snapshot = bp.buffer.read(cx).snapshot();
 715                bp.breakpoints
 716                    .iter()
 717                    .map(|bp| {
 718                        let position = snapshot.summary_for_anchor::<PointUtf16>(bp.position()).row;
 719                        let bp = &bp.bp;
 720                        SourceBreakpoint {
 721                            row: position,
 722                            path: path.clone(),
 723                            state: bp.bp.state,
 724                            message: bp.bp.message.clone(),
 725                            condition: bp.bp.condition.clone(),
 726                            hit_condition: bp.bp.hit_condition.clone(),
 727                        }
 728                    })
 729                    .collect()
 730            })
 731            .unwrap_or_default()
 732    }
 733
 734    pub fn all_breakpoints(&self) -> BTreeMap<Arc<Path>, Vec<BreakpointWithPosition>> {
 735        self.breakpoints
 736            .iter()
 737            .map(|(path, bp)| {
 738                (
 739                    path.clone(),
 740                    bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(),
 741                )
 742            })
 743            .collect()
 744    }
 745    pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
 746        self.breakpoints
 747            .iter()
 748            .map(|(path, bp)| {
 749                let snapshot = bp.buffer.read(cx).snapshot();
 750                (
 751                    path.clone(),
 752                    bp.breakpoints
 753                        .iter()
 754                        .map(|breakpoint| {
 755                            let position = snapshot
 756                                .summary_for_anchor::<PointUtf16>(breakpoint.position())
 757                                .row;
 758                            let breakpoint = &breakpoint.bp;
 759                            SourceBreakpoint {
 760                                row: position,
 761                                path: path.clone(),
 762                                message: breakpoint.bp.message.clone(),
 763                                state: breakpoint.bp.state,
 764                                hit_condition: breakpoint.bp.hit_condition.clone(),
 765                                condition: breakpoint.bp.condition.clone(),
 766                            }
 767                        })
 768                        .collect(),
 769                )
 770            })
 771            .collect()
 772    }
 773
 774    pub fn with_serialized_breakpoints(
 775        &self,
 776        breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
 777        cx: &mut Context<BreakpointStore>,
 778    ) -> Task<Result<()>> {
 779        if let BreakpointStoreMode::Local(mode) = &self.mode {
 780            let mode = mode.clone();
 781            cx.spawn(async move |this, cx| {
 782                let mut new_breakpoints = BTreeMap::default();
 783                for (path, bps) in breakpoints {
 784                    if bps.is_empty() {
 785                        continue;
 786                    }
 787                    let (worktree, relative_path) = mode
 788                        .worktree_store
 789                        .update(cx, |this, cx| {
 790                            this.find_or_create_worktree(&path, false, cx)
 791                        })?
 792                        .await?;
 793                    let buffer = mode
 794                        .buffer_store
 795                        .update(cx, |this, cx| {
 796                            let path = ProjectPath {
 797                                worktree_id: worktree.read(cx).id(),
 798                                path: relative_path.into(),
 799                            };
 800                            this.open_buffer(path, cx)
 801                        })?
 802                        .await;
 803                    let Ok(buffer) = buffer else {
 804                        log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
 805                        continue;
 806                    };
 807                    let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
 808
 809                    let mut breakpoints_for_file =
 810                        this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
 811
 812                    for bp in bps {
 813                        let max_point = snapshot.max_point_utf16();
 814                        let point = PointUtf16::new(bp.row, 0);
 815                        if point > max_point {
 816                            log::error!("skipping a deserialized breakpoint that's out of range");
 817                            continue;
 818                        }
 819                        let position = snapshot.anchor_after(point);
 820                        breakpoints_for_file
 821                            .breakpoints
 822                            .push(StatefulBreakpoint::new(BreakpointWithPosition {
 823                                position,
 824                                bp: Breakpoint {
 825                                    message: bp.message,
 826                                    state: bp.state,
 827                                    condition: bp.condition,
 828                                    hit_condition: bp.hit_condition,
 829                                },
 830                            }))
 831                    }
 832                    new_breakpoints.insert(path, breakpoints_for_file);
 833                }
 834                this.update(cx, |this, cx| {
 835                    log::info!("Finish deserializing breakpoints & initializing breakpoint store");
 836                    for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
 837                        (path.to_string_lossy(), bp_in_file.breakpoints.len())
 838                    }) {
 839                        let breakpoint_str = if count > 1 {
 840                            "breakpoints"
 841                        } else {
 842                            "breakpoint"
 843                        };
 844                        log::debug!("Deserialized {count} {breakpoint_str} at path: {path}");
 845                    }
 846
 847                    this.breakpoints = new_breakpoints;
 848
 849                    cx.notify();
 850                })?;
 851
 852                Ok(())
 853            })
 854        } else {
 855            Task::ready(Ok(()))
 856        }
 857    }
 858
 859    #[cfg(any(test, feature = "test-support"))]
 860    pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
 861        self.breakpoints.keys().cloned().collect()
 862    }
 863}
 864
 865#[derive(Clone, Copy)]
 866pub enum BreakpointUpdatedReason {
 867    Toggled,
 868    FileSaved,
 869}
 870
 871pub enum BreakpointStoreEvent {
 872    SetDebugLine,
 873    ClearDebugLines,
 874    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
 875    BreakpointsCleared(Vec<Arc<Path>>),
 876}
 877
 878impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
 879
 880type BreakpointMessage = Arc<str>;
 881
 882#[derive(Clone, Debug)]
 883pub enum BreakpointEditAction {
 884    Toggle,
 885    InvertState,
 886    EditLogMessage(BreakpointMessage),
 887    EditCondition(BreakpointMessage),
 888    EditHitCondition(BreakpointMessage),
 889}
 890
 891#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
 892pub enum BreakpointState {
 893    Enabled,
 894    Disabled,
 895}
 896
 897impl BreakpointState {
 898    #[inline]
 899    pub fn is_enabled(&self) -> bool {
 900        matches!(self, BreakpointState::Enabled)
 901    }
 902
 903    #[inline]
 904    pub fn is_disabled(&self) -> bool {
 905        matches!(self, BreakpointState::Disabled)
 906    }
 907
 908    #[inline]
 909    pub fn to_int(&self) -> i32 {
 910        match self {
 911            BreakpointState::Enabled => 0,
 912            BreakpointState::Disabled => 1,
 913        }
 914    }
 915}
 916
 917#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 918pub struct Breakpoint {
 919    pub message: Option<BreakpointMessage>,
 920    /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
 921    pub hit_condition: Option<Arc<str>>,
 922    pub condition: Option<BreakpointMessage>,
 923    pub state: BreakpointState,
 924}
 925
 926impl Breakpoint {
 927    pub fn new_standard() -> Self {
 928        Self {
 929            state: BreakpointState::Enabled,
 930            hit_condition: None,
 931            condition: None,
 932            message: None,
 933        }
 934    }
 935
 936    pub fn new_condition(hit_condition: &str) -> Self {
 937        Self {
 938            state: BreakpointState::Enabled,
 939            condition: None,
 940            hit_condition: Some(hit_condition.into()),
 941            message: None,
 942        }
 943    }
 944
 945    pub fn new_log(log_message: &str) -> Self {
 946        Self {
 947            state: BreakpointState::Enabled,
 948            hit_condition: None,
 949            condition: None,
 950            message: Some(log_message.into()),
 951        }
 952    }
 953
 954    fn to_proto(
 955        &self,
 956        _path: &Path,
 957        position: &text::Anchor,
 958        session_states: &HashMap<SessionId, BreakpointSessionState>,
 959    ) -> Option<client::proto::Breakpoint> {
 960        Some(client::proto::Breakpoint {
 961            position: Some(serialize_text_anchor(position)),
 962            state: match self.state {
 963                BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
 964                BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
 965            },
 966            message: self.message.as_ref().map(|s| String::from(s.as_ref())),
 967            condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
 968            hit_condition: self
 969                .hit_condition
 970                .as_ref()
 971                .map(|s| String::from(s.as_ref())),
 972            session_state: session_states
 973                .iter()
 974                .map(|(session_id, state)| {
 975                    (
 976                        session_id.to_proto(),
 977                        proto::BreakpointSessionState {
 978                            id: state.id,
 979                            verified: state.verified,
 980                        },
 981                    )
 982                })
 983                .collect(),
 984        })
 985    }
 986
 987    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
 988        Some(Self {
 989            state: match proto::BreakpointState::from_i32(breakpoint.state) {
 990                Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
 991                None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
 992            },
 993            message: breakpoint.message.map(Into::into),
 994            condition: breakpoint.condition.map(Into::into),
 995            hit_condition: breakpoint.hit_condition.map(Into::into),
 996        })
 997    }
 998
 999    #[inline]
1000    pub fn is_enabled(&self) -> bool {
1001        self.state.is_enabled()
1002    }
1003
1004    #[inline]
1005    pub fn is_disabled(&self) -> bool {
1006        self.state.is_disabled()
1007    }
1008}
1009
1010/// Breakpoint for location within source code.
1011#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1012pub struct SourceBreakpoint {
1013    pub row: u32,
1014    pub path: Arc<Path>,
1015    pub message: Option<Arc<str>>,
1016    pub condition: Option<Arc<str>>,
1017    pub hit_condition: Option<Arc<str>>,
1018    pub state: BreakpointState,
1019}
1020
1021impl From<SourceBreakpoint> for dap::SourceBreakpoint {
1022    fn from(bp: SourceBreakpoint) -> Self {
1023        Self {
1024            line: bp.row as u64 + 1,
1025            column: None,
1026            condition: bp
1027                .condition
1028                .map(|condition| String::from(condition.as_ref())),
1029            hit_condition: bp
1030                .hit_condition
1031                .map(|hit_condition| String::from(hit_condition.as_ref())),
1032            log_message: bp.message.map(|message| String::from(message.as_ref())),
1033            mode: None,
1034        }
1035    }
1036}