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