breakpoint_store.rs

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