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        log::error!("breakpoint store init");
 168        client.add_entity_request_handler(Self::handle_toggle_breakpoint);
 169        client.add_entity_message_handler(Self::handle_breakpoints_for_file);
 170    }
 171    pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
 172        BreakpointStore {
 173            breakpoints: BTreeMap::new(),
 174            mode: BreakpointStoreMode::Local(LocalBreakpointStore {
 175                worktree_store,
 176                buffer_store,
 177            }),
 178            downstream_client: None,
 179            active_stack_frame: Default::default(),
 180        }
 181    }
 182
 183    pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
 184        BreakpointStore {
 185            breakpoints: BTreeMap::new(),
 186            mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
 187                upstream_client,
 188                _upstream_project_id: upstream_project_id,
 189            }),
 190            downstream_client: None,
 191            active_stack_frame: Default::default(),
 192        }
 193    }
 194
 195    pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
 196        self.downstream_client = Some((downstream_client, project_id));
 197    }
 198
 199    pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) {
 200        self.downstream_client.take();
 201
 202        cx.notify();
 203    }
 204
 205    async fn handle_breakpoints_for_file(
 206        this: Entity<Project>,
 207        message: TypedEnvelope<proto::BreakpointsForFile>,
 208        mut cx: AsyncApp,
 209    ) -> Result<()> {
 210        let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
 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 =
 218                    this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
 219                Some(this.open_buffer(path, cx))
 220            })
 221            .ok()
 222            .flatten()
 223            .context("Invalid project path")?
 224            .await?;
 225
 226        breakpoints.update(&mut cx, move |this, cx| {
 227            let bps = this
 228                .breakpoints
 229                .entry(Arc::<Path>::from(message.payload.path.as_ref()))
 230                .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
 231
 232            bps.breakpoints = message
 233                .payload
 234                .breakpoints
 235                .into_iter()
 236                .filter_map(|breakpoint| {
 237                    let position =
 238                        language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
 239                    let session_state = breakpoint
 240                        .session_state
 241                        .iter()
 242                        .map(|(session_id, state)| {
 243                            let state = BreakpointSessionState {
 244                                id: state.id,
 245                                verified: state.verified,
 246                            };
 247                            (SessionId::from_proto(*session_id), state)
 248                        })
 249                        .collect();
 250                    let breakpoint = Breakpoint::from_proto(breakpoint)?;
 251                    let bp = BreakpointWithPosition {
 252                        position,
 253                        bp: breakpoint,
 254                    };
 255
 256                    Some(StatefulBreakpoint { bp, session_state })
 257                })
 258                .collect();
 259
 260            cx.notify();
 261        })?;
 262
 263        Ok(())
 264    }
 265
 266    async fn handle_toggle_breakpoint(
 267        this: Entity<Project>,
 268        message: TypedEnvelope<proto::ToggleBreakpoint>,
 269        mut cx: AsyncApp,
 270    ) -> Result<proto::Ack> {
 271        let breakpoints = this.read_with(&cx, |this, _| this.breakpoint_store())?;
 272        let path = this
 273            .update(&mut cx, |this, cx| {
 274                this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)
 275            })?
 276            .context("Could not resolve provided abs path")?;
 277        let buffer = this
 278            .update(&mut cx, |this, cx| {
 279                this.buffer_store().read(cx).get_by_path(&path)
 280            })?
 281            .context("Could not find buffer for a given path")?;
 282        let breakpoint = message
 283            .payload
 284            .breakpoint
 285            .context("Breakpoint not present in RPC payload")?;
 286        let position = language::proto::deserialize_anchor(
 287            breakpoint
 288                .position
 289                .clone()
 290                .context("Anchor not present in RPC payload")?,
 291        )
 292        .context("Anchor deserialization failed")?;
 293        let breakpoint =
 294            Breakpoint::from_proto(breakpoint).context("Could not deserialize breakpoint")?;
 295
 296        breakpoints.update(&mut cx, |this, cx| {
 297            this.toggle_breakpoint(
 298                buffer,
 299                BreakpointWithPosition {
 300                    position,
 301                    bp: breakpoint,
 302                },
 303                BreakpointEditAction::Toggle,
 304                cx,
 305            );
 306        })?;
 307        Ok(proto::Ack {})
 308    }
 309
 310    pub(crate) fn broadcast(&self) {
 311        if let Some((client, project_id)) = &self.downstream_client {
 312            for (path, breakpoint_set) in &self.breakpoints {
 313                let _ = client.send(proto::BreakpointsForFile {
 314                    project_id: *project_id,
 315                    path: path.to_str().map(ToOwned::to_owned).unwrap(),
 316                    breakpoints: breakpoint_set
 317                        .breakpoints
 318                        .iter()
 319                        .filter_map(|breakpoint| {
 320                            breakpoint.bp.bp.to_proto(
 321                                path,
 322                                breakpoint.position(),
 323                                &breakpoint.session_state,
 324                            )
 325                        })
 326                        .collect(),
 327                });
 328            }
 329        }
 330    }
 331
 332    pub(crate) fn update_session_breakpoint(
 333        &mut self,
 334        session_id: SessionId,
 335        _: dap::BreakpointEventReason,
 336        breakpoint: dap::Breakpoint,
 337    ) {
 338        maybe!({
 339            let event_id = breakpoint.id?;
 340
 341            let state = self
 342                .breakpoints
 343                .values_mut()
 344                .find_map(|breakpoints_in_file| {
 345                    breakpoints_in_file
 346                        .breakpoints
 347                        .iter_mut()
 348                        .find_map(|state| {
 349                            let state = state.session_state.get_mut(&session_id)?;
 350
 351                            if state.id == event_id {
 352                                Some(state)
 353                            } else {
 354                                None
 355                            }
 356                        })
 357                })?;
 358
 359            state.verified = breakpoint.verified;
 360            Some(())
 361        });
 362    }
 363
 364    pub(super) fn mark_breakpoints_verified(
 365        &mut self,
 366        session_id: SessionId,
 367        abs_path: &Path,
 368
 369        it: impl Iterator<Item = (BreakpointWithPosition, BreakpointSessionState)>,
 370    ) {
 371        maybe!({
 372            let breakpoints = self.breakpoints.get_mut(abs_path)?;
 373            for (breakpoint, state) in it {
 374                if let Some(to_update) = breakpoints
 375                    .breakpoints
 376                    .iter_mut()
 377                    .find(|bp| *bp.position() == breakpoint.position)
 378                {
 379                    to_update
 380                        .session_state
 381                        .entry(session_id)
 382                        .insert_entry(state);
 383                }
 384            }
 385            Some(())
 386        });
 387    }
 388
 389    pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
 390        worktree::File::from_dyn(buffer.read(cx).file())
 391            .map(|file| file.worktree.read(cx).absolutize(&file.path))
 392            .map(Arc::<Path>::from)
 393    }
 394
 395    pub fn toggle_breakpoint(
 396        &mut self,
 397        buffer: Entity<Buffer>,
 398        mut breakpoint: BreakpointWithPosition,
 399        edit_action: BreakpointEditAction,
 400        cx: &mut Context<Self>,
 401    ) {
 402        let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
 403            return;
 404        };
 405
 406        let breakpoint_set = self
 407            .breakpoints
 408            .entry(abs_path.clone())
 409            .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
 410
 411        match edit_action {
 412            BreakpointEditAction::Toggle => {
 413                let len_before = breakpoint_set.breakpoints.len();
 414                breakpoint_set
 415                    .breakpoints
 416                    .retain(|value| breakpoint != value.bp);
 417                if len_before == breakpoint_set.breakpoints.len() {
 418                    // We did not remove any breakpoint, hence let's toggle one.
 419                    breakpoint_set
 420                        .breakpoints
 421                        .push(StatefulBreakpoint::new(breakpoint.clone()));
 422                }
 423            }
 424            BreakpointEditAction::InvertState => {
 425                if let Some(bp) = breakpoint_set
 426                    .breakpoints
 427                    .iter_mut()
 428                    .find(|value| breakpoint == value.bp)
 429                {
 430                    let bp = &mut bp.bp.bp;
 431                    if bp.is_enabled() {
 432                        bp.state = BreakpointState::Disabled;
 433                    } else {
 434                        bp.state = BreakpointState::Enabled;
 435                    }
 436                } else {
 437                    breakpoint.bp.state = BreakpointState::Disabled;
 438                    breakpoint_set
 439                        .breakpoints
 440                        .push(StatefulBreakpoint::new(breakpoint.clone()));
 441                }
 442            }
 443            BreakpointEditAction::EditLogMessage(log_message) => {
 444                if !log_message.is_empty() {
 445                    let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| {
 446                        if breakpoint.position == *bp.position() {
 447                            Some(&mut bp.bp.bp)
 448                        } else {
 449                            None
 450                        }
 451                    });
 452
 453                    if let Some(found_bp) = found_bp {
 454                        found_bp.message = Some(log_message);
 455                    } else {
 456                        breakpoint.bp.message = Some(log_message);
 457                        // We did not remove any breakpoint, hence let's toggle one.
 458                        breakpoint_set
 459                            .breakpoints
 460                            .push(StatefulBreakpoint::new(breakpoint.clone()));
 461                    }
 462                } else if breakpoint.bp.message.is_some() {
 463                    if let Some(position) = breakpoint_set
 464                        .breakpoints
 465                        .iter()
 466                        .find_position(|other| breakpoint == other.bp)
 467                        .map(|res| res.0)
 468                    {
 469                        breakpoint_set.breakpoints.remove(position);
 470                    } else {
 471                        log::error!("Failed to find position of breakpoint to delete")
 472                    }
 473                }
 474            }
 475            BreakpointEditAction::EditHitCondition(hit_condition) => {
 476                if !hit_condition.is_empty() {
 477                    let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
 478                        if breakpoint.position == *other.position() {
 479                            Some(&mut other.bp.bp)
 480                        } else {
 481                            None
 482                        }
 483                    });
 484
 485                    if let Some(found_bp) = found_bp {
 486                        found_bp.hit_condition = Some(hit_condition);
 487                    } else {
 488                        breakpoint.bp.hit_condition = Some(hit_condition);
 489                        // We did not remove any breakpoint, hence let's toggle one.
 490                        breakpoint_set
 491                            .breakpoints
 492                            .push(StatefulBreakpoint::new(breakpoint.clone()))
 493                    }
 494                } else if breakpoint.bp.hit_condition.is_some() {
 495                    if let Some(position) = breakpoint_set
 496                        .breakpoints
 497                        .iter()
 498                        .find_position(|bp| breakpoint == bp.bp)
 499                        .map(|res| res.0)
 500                    {
 501                        breakpoint_set.breakpoints.remove(position);
 502                    } else {
 503                        log::error!("Failed to find position of breakpoint to delete")
 504                    }
 505                }
 506            }
 507            BreakpointEditAction::EditCondition(condition) => {
 508                if !condition.is_empty() {
 509                    let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
 510                        if breakpoint.position == *other.position() {
 511                            Some(&mut other.bp.bp)
 512                        } else {
 513                            None
 514                        }
 515                    });
 516
 517                    if let Some(found_bp) = found_bp {
 518                        found_bp.condition = Some(condition);
 519                    } else {
 520                        breakpoint.bp.condition = Some(condition);
 521                        // We did not remove any breakpoint, hence let's toggle one.
 522                        breakpoint_set
 523                            .breakpoints
 524                            .push(StatefulBreakpoint::new(breakpoint.clone()));
 525                    }
 526                } else if breakpoint.bp.condition.is_some() {
 527                    if let Some(position) = breakpoint_set
 528                        .breakpoints
 529                        .iter()
 530                        .find_position(|bp| breakpoint == bp.bp)
 531                        .map(|res| res.0)
 532                    {
 533                        breakpoint_set.breakpoints.remove(position);
 534                    } else {
 535                        log::error!("Failed to find position of breakpoint to delete")
 536                    }
 537                }
 538            }
 539        }
 540
 541        if breakpoint_set.breakpoints.is_empty() {
 542            self.breakpoints.remove(&abs_path);
 543        }
 544        if let BreakpointStoreMode::Remote(remote) = &self.mode {
 545            if let Some(breakpoint) =
 546                breakpoint
 547                    .bp
 548                    .to_proto(&abs_path, &breakpoint.position, &HashMap::default())
 549            {
 550                cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
 551                    project_id: remote._upstream_project_id,
 552                    path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
 553                    breakpoint: Some(breakpoint),
 554                }))
 555                .detach();
 556            }
 557        } else if let Some((client, project_id)) = &self.downstream_client {
 558            let breakpoints = self
 559                .breakpoints
 560                .get(&abs_path)
 561                .map(|breakpoint_set| {
 562                    breakpoint_set
 563                        .breakpoints
 564                        .iter()
 565                        .filter_map(|bp| {
 566                            bp.bp
 567                                .bp
 568                                .to_proto(&abs_path, bp.position(), &bp.session_state)
 569                        })
 570                        .collect()
 571                })
 572                .unwrap_or_default();
 573
 574            let _ = client.send(proto::BreakpointsForFile {
 575                project_id: *project_id,
 576                path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
 577                breakpoints,
 578            });
 579        }
 580
 581        cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
 582            abs_path,
 583            BreakpointUpdatedReason::Toggled,
 584        ));
 585        cx.notify();
 586    }
 587
 588    pub fn on_file_rename(
 589        &mut self,
 590        old_path: Arc<Path>,
 591        new_path: Arc<Path>,
 592        cx: &mut Context<Self>,
 593    ) {
 594        if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
 595            self.breakpoints.insert(new_path, breakpoints);
 596
 597            cx.notify();
 598        }
 599    }
 600
 601    pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
 602        let breakpoint_paths = self.breakpoints.keys().cloned().collect();
 603        self.breakpoints.clear();
 604        cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
 605    }
 606
 607    pub fn breakpoints<'a>(
 608        &'a self,
 609        buffer: &'a Entity<Buffer>,
 610        range: Option<Range<text::Anchor>>,
 611        buffer_snapshot: &'a BufferSnapshot,
 612        cx: &App,
 613    ) -> impl Iterator<Item = (&'a BreakpointWithPosition, Option<BreakpointSessionState>)> + 'a
 614    {
 615        let abs_path = Self::abs_path_from_buffer(buffer, cx);
 616        let active_session_id = self
 617            .active_stack_frame
 618            .as_ref()
 619            .map(|frame| frame.session_id);
 620        abs_path
 621            .and_then(|path| self.breakpoints.get(&path))
 622            .into_iter()
 623            .flat_map(move |file_breakpoints| {
 624                file_breakpoints.breakpoints.iter().filter_map({
 625                    let range = range.clone();
 626                    move |bp| {
 627                        if let Some(range) = &range
 628                            && (bp.position().cmp(&range.start, buffer_snapshot).is_lt()
 629                                || bp.position().cmp(&range.end, buffer_snapshot).is_gt())
 630                        {
 631                            return None;
 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,
 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                    for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
 836                        (path.to_string_lossy(), bp_in_file.breakpoints.len())
 837                    }) {
 838                        let breakpoint_str = if count > 1 {
 839                            "breakpoints"
 840                        } else {
 841                            "breakpoint"
 842                        };
 843                        log::debug!("Deserialized {count} {breakpoint_str} at path: {path}");
 844                    }
 845
 846                    this.breakpoints = new_breakpoints;
 847
 848                    cx.notify();
 849                })?;
 850
 851                Ok(())
 852            })
 853        } else {
 854            Task::ready(Ok(()))
 855        }
 856    }
 857
 858    #[cfg(any(test, feature = "test-support"))]
 859    pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
 860        self.breakpoints.keys().cloned().collect()
 861    }
 862}
 863
 864#[derive(Clone, Copy)]
 865pub enum BreakpointUpdatedReason {
 866    Toggled,
 867    FileSaved,
 868}
 869
 870pub enum BreakpointStoreEvent {
 871    SetDebugLine,
 872    ClearDebugLines,
 873    BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
 874    BreakpointsCleared(Vec<Arc<Path>>),
 875}
 876
 877impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
 878
 879type BreakpointMessage = Arc<str>;
 880
 881#[derive(Clone, Debug)]
 882pub enum BreakpointEditAction {
 883    Toggle,
 884    InvertState,
 885    EditLogMessage(BreakpointMessage),
 886    EditCondition(BreakpointMessage),
 887    EditHitCondition(BreakpointMessage),
 888}
 889
 890#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
 891pub enum BreakpointState {
 892    Enabled,
 893    Disabled,
 894}
 895
 896impl BreakpointState {
 897    #[inline]
 898    pub fn is_enabled(&self) -> bool {
 899        matches!(self, BreakpointState::Enabled)
 900    }
 901
 902    #[inline]
 903    pub fn is_disabled(&self) -> bool {
 904        matches!(self, BreakpointState::Disabled)
 905    }
 906
 907    #[inline]
 908    pub fn to_int(self) -> i32 {
 909        match self {
 910            BreakpointState::Enabled => 0,
 911            BreakpointState::Disabled => 1,
 912        }
 913    }
 914}
 915
 916#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 917pub struct Breakpoint {
 918    pub message: Option<BreakpointMessage>,
 919    /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
 920    pub hit_condition: Option<Arc<str>>,
 921    pub condition: Option<BreakpointMessage>,
 922    pub state: BreakpointState,
 923}
 924
 925impl Breakpoint {
 926    pub fn new_standard() -> Self {
 927        Self {
 928            state: BreakpointState::Enabled,
 929            hit_condition: None,
 930            condition: None,
 931            message: None,
 932        }
 933    }
 934
 935    pub fn new_condition(hit_condition: &str) -> Self {
 936        Self {
 937            state: BreakpointState::Enabled,
 938            condition: None,
 939            hit_condition: Some(hit_condition.into()),
 940            message: None,
 941        }
 942    }
 943
 944    pub fn new_log(log_message: &str) -> Self {
 945        Self {
 946            state: BreakpointState::Enabled,
 947            hit_condition: None,
 948            condition: None,
 949            message: Some(log_message.into()),
 950        }
 951    }
 952
 953    fn to_proto(
 954        &self,
 955        _path: &Path,
 956        position: &text::Anchor,
 957        session_states: &HashMap<SessionId, BreakpointSessionState>,
 958    ) -> Option<client::proto::Breakpoint> {
 959        Some(client::proto::Breakpoint {
 960            position: Some(serialize_text_anchor(position)),
 961            state: match self.state {
 962                BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
 963                BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
 964            },
 965            message: self.message.as_ref().map(|s| String::from(s.as_ref())),
 966            condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
 967            hit_condition: self
 968                .hit_condition
 969                .as_ref()
 970                .map(|s| String::from(s.as_ref())),
 971            session_state: session_states
 972                .iter()
 973                .map(|(session_id, state)| {
 974                    (
 975                        session_id.to_proto(),
 976                        proto::BreakpointSessionState {
 977                            id: state.id,
 978                            verified: state.verified,
 979                        },
 980                    )
 981                })
 982                .collect(),
 983        })
 984    }
 985
 986    fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
 987        Some(Self {
 988            state: match proto::BreakpointState::from_i32(breakpoint.state) {
 989                Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
 990                None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
 991            },
 992            message: breakpoint.message.map(Into::into),
 993            condition: breakpoint.condition.map(Into::into),
 994            hit_condition: breakpoint.hit_condition.map(Into::into),
 995        })
 996    }
 997
 998    #[inline]
 999    pub fn is_enabled(&self) -> bool {
1000        self.state.is_enabled()
1001    }
1002
1003    #[inline]
1004    pub fn is_disabled(&self) -> bool {
1005        self.state.is_disabled()
1006    }
1007}
1008
1009/// Breakpoint for location within source code.
1010#[derive(Clone, Debug, Hash, PartialEq, Eq)]
1011pub struct SourceBreakpoint {
1012    pub row: u32,
1013    pub path: Arc<Path>,
1014    pub message: Option<Arc<str>>,
1015    pub condition: Option<Arc<str>>,
1016    pub hit_condition: Option<Arc<str>>,
1017    pub state: BreakpointState,
1018}
1019
1020impl From<SourceBreakpoint> for dap::SourceBreakpoint {
1021    fn from(bp: SourceBreakpoint) -> Self {
1022        Self {
1023            line: bp.row as u64 + 1,
1024            column: None,
1025            condition: bp
1026                .condition
1027                .map(|condition| String::from(condition.as_ref())),
1028            hit_condition: bp
1029                .hit_condition
1030                .map(|hit_condition| String::from(hit_condition.as_ref())),
1031            log_message: bp.message.map(|message| String::from(message.as_ref())),
1032            mode: None,
1033        }
1034    }
1035}