1082 lines · 38.3 KB · Rust
RawDownload
//! Module for managing breakpoints in a project.//!//! 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.use anyhow::{Context as _, Result};pub use breakpoints_in_file::{BreakpointSessionState, BreakpointWithPosition};use breakpoints_in_file::{BreakpointsInFile, StatefulBreakpoint};use collections::{BTreeMap, HashMap};use dap::{StackFrameId, client::SessionId};use gpui::{    App, AppContext, AsyncApp, Context, Entity, EntityId, EventEmitter, Subscription, Task,};use itertools::Itertools;use language::{Buffer, BufferSnapshot, proto::serialize_anchor as serialize_text_anchor};use rpc::{    AnyProtoClient, TypedEnvelope,    proto::{self},};use std::{hash::Hash, ops::Range, path::Path, sync::Arc, u32};use text::{Bias, Point, PointUtf16, Unclipped};use util::maybe; use crate::{ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore}; use super::session::ThreadId; mod breakpoints_in_file {    use collections::HashMap;    use language::BufferEvent;     use super::*;     #[derive(Clone, Debug, PartialEq, Eq)]    pub struct BreakpointWithPosition {        pub position: text::Anchor,        pub bp: Breakpoint,    }     /// A breakpoint with per-session data about it's state (as seen by the Debug Adapter).    #[derive(Clone, Debug)]    pub struct StatefulBreakpoint {        pub bp: BreakpointWithPosition,        pub session_state: HashMap<SessionId, BreakpointSessionState>,    }     impl StatefulBreakpoint {        pub(super) fn new(bp: BreakpointWithPosition) -> Self {            Self {                bp,                session_state: Default::default(),            }        }        pub(super) fn position(&self) -> &text::Anchor {            &self.bp.position        }    }     #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]    pub struct BreakpointSessionState {        /// Session-specific identifier for the breakpoint, as assigned by Debug Adapter.        pub id: u64,        pub verified: bool,    }    #[derive(Clone)]    pub(super) struct BreakpointsInFile {        pub(super) buffer: Entity<Buffer>,        // 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        pub(super) breakpoints: Vec<StatefulBreakpoint>,        _subscription: Arc<Subscription>,    }     impl BreakpointsInFile {        pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {            let subscription = Arc::from(cx.subscribe(                &buffer,                |breakpoint_store, buffer, event, cx| match event {                    BufferEvent::Saved => {                        if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {                            cx.emit(BreakpointStoreEvent::BreakpointsUpdated(                                abs_path,                                BreakpointUpdatedReason::FileSaved,                            ));                        }                    }                    BufferEvent::FileHandleChanged => {                        let entity_id = buffer.entity_id();                         if buffer.read(cx).file().is_none_or(|f| f.disk_state().is_deleted()) {                            breakpoint_store.breakpoints.retain(|_, breakpoints_in_file| {                                breakpoints_in_file.buffer.entity_id() != entity_id                            });                             cx.notify();                            return;                        }                         if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {                            if breakpoint_store.breakpoints.contains_key(&abs_path) {                                return;                            }                             if let Some(old_path) = breakpoint_store                                .breakpoints                                .iter()                                .find(|(_, in_file)| in_file.buffer.entity_id() == entity_id)                                .map(|values| values.0)                                .cloned()                            {                                let Some(breakpoints_in_file) =                                    breakpoint_store.breakpoints.remove(&old_path) else {                                        log::error!("Couldn't get breakpoints in file from old path during buffer rename handling");                                        return;                                    };                                 breakpoint_store.breakpoints.insert(abs_path, breakpoints_in_file);                                cx.notify();                            }                        }                    }                    _ => {}                },            ));             BreakpointsInFile {                buffer,                breakpoints: Vec::new(),                _subscription: subscription,            }        }    }} #[derive(Clone)]struct RemoteBreakpointStore {    upstream_client: AnyProtoClient,    upstream_project_id: u64,} #[derive(Clone)]enum BreakpointStoreMode {    Local,    Remote(RemoteBreakpointStore),} #[derive(Clone, PartialEq)]pub struct ActiveStackFrame {    pub session_id: SessionId,    pub thread_id: ThreadId,    pub stack_frame_id: StackFrameId,    pub path: Arc<Path>,    pub position: text::Anchor,} pub struct BreakpointStore {    buffer_store: Entity<BufferStore>,    worktree_store: Entity<WorktreeStore>,    breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,    downstream_client: Option<(AnyProtoClient, u64)>,    active_stack_frame: Option<ActiveStackFrame>,    active_debug_line_pane_id: Option<EntityId>,    // E.g ssh    mode: BreakpointStoreMode,} impl BreakpointStore {    pub fn init(client: &AnyProtoClient) {        client.add_entity_request_handler(Self::handle_toggle_breakpoint);        client.add_entity_message_handler(Self::handle_breakpoints_for_file);    }    pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {        BreakpointStore {            breakpoints: BTreeMap::new(),            mode: BreakpointStoreMode::Local,            buffer_store,            worktree_store,            downstream_client: None,            active_stack_frame: Default::default(),            active_debug_line_pane_id: None,        }    }     pub(crate) fn remote(        upstream_project_id: u64,        upstream_client: AnyProtoClient,        buffer_store: Entity<BufferStore>,        worktree_store: Entity<WorktreeStore>,    ) -> Self {        BreakpointStore {            breakpoints: BTreeMap::new(),            mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {                upstream_client,                upstream_project_id,            }),            buffer_store,            worktree_store,            downstream_client: None,            active_stack_frame: Default::default(),            active_debug_line_pane_id: None,        }    }     pub fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {        self.downstream_client = Some((downstream_client, project_id));    }     pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) {        self.downstream_client.take();         cx.notify();    }     async fn handle_breakpoints_for_file(        this: Entity<Self>,        message: TypedEnvelope<proto::BreakpointsForFile>,        mut cx: AsyncApp,    ) -> Result<()> {        if message.payload.breakpoints.is_empty() {            return Ok(());        }         let buffer = this            .update(&mut cx, |this, cx| {                let path = this                    .worktree_store                    .read(cx)                    .project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;                Some(                    this.buffer_store                        .update(cx, |this, cx| this.open_buffer(path, cx)),                )            })            .context("Invalid project path")?            .await?;         this.update(&mut cx, move |this, cx| {            let bps = this                .breakpoints                .entry(Arc::<Path>::from(message.payload.path.as_ref()))                .or_insert_with(|| BreakpointsInFile::new(buffer, cx));             bps.breakpoints = message                .payload                .breakpoints                .into_iter()                .filter_map(|breakpoint| {                    let position =                        language::proto::deserialize_anchor(breakpoint.position.clone()?)?;                    let session_state = breakpoint                        .session_state                        .iter()                        .map(|(session_id, state)| {                            let state = BreakpointSessionState {                                id: state.id,                                verified: state.verified,                            };                            (SessionId::from_proto(*session_id), state)                        })                        .collect();                    let breakpoint = Breakpoint::from_proto(breakpoint)?;                    let bp = BreakpointWithPosition {                        position,                        bp: breakpoint,                    };                     Some(StatefulBreakpoint { bp, session_state })                })                .collect();             cx.notify();        });         Ok(())    }     async fn handle_toggle_breakpoint(        this: Entity<Self>,        message: TypedEnvelope<proto::ToggleBreakpoint>,        mut cx: AsyncApp,    ) -> Result<proto::Ack> {        let path = this            .update(&mut cx, |this, cx| {                this.worktree_store                    .read(cx)                    .project_path_for_absolute_path(message.payload.path.as_ref(), cx)            })            .context("Could not resolve provided abs path")?;        let buffer = this            .update(&mut cx, |this, cx| {                this.buffer_store.read(cx).get_by_path(&path)            })            .context("Could not find buffer for a given path")?;        let breakpoint = message            .payload            .breakpoint            .context("Breakpoint not present in RPC payload")?;        let position = language::proto::deserialize_anchor(            breakpoint                .position                .clone()                .context("Anchor not present in RPC payload")?,        )        .context("Anchor deserialization failed")?;        let breakpoint =            Breakpoint::from_proto(breakpoint).context("Could not deserialize breakpoint")?;         this.update(&mut cx, |this, cx| {            this.toggle_breakpoint(                buffer,                BreakpointWithPosition {                    position,                    bp: breakpoint,                },                BreakpointEditAction::Toggle,                cx,            );        });        Ok(proto::Ack {})    }     pub(crate) fn broadcast(&self) {        if let Some((client, project_id)) = &self.downstream_client {            for (path, breakpoint_set) in &self.breakpoints {                let _ = client.send(proto::BreakpointsForFile {                    project_id: *project_id,                    path: path.to_string_lossy().into_owned(),                    breakpoints: breakpoint_set                        .breakpoints                        .iter()                        .filter_map(|breakpoint| {                            breakpoint.bp.bp.to_proto(                                path,                                breakpoint.position(),                                &breakpoint.session_state,                            )                        })                        .collect(),                });            }        }    }     pub(crate) fn update_session_breakpoint(        &mut self,        session_id: SessionId,        _: dap::BreakpointEventReason,        breakpoint: dap::Breakpoint,    ) {        maybe!({            let event_id = breakpoint.id?;             let state = self                .breakpoints                .values_mut()                .find_map(|breakpoints_in_file| {                    breakpoints_in_file                        .breakpoints                        .iter_mut()                        .find_map(|state| {                            let state = state.session_state.get_mut(&session_id)?;                             if state.id == event_id {                                Some(state)                            } else {                                None                            }                        })                })?;             state.verified = breakpoint.verified;            Some(())        });    }     pub(super) fn mark_breakpoints_verified(        &mut self,        session_id: SessionId,        abs_path: &Path,         it: impl Iterator<Item = (BreakpointWithPosition, BreakpointSessionState)>,    ) {        maybe!({            let breakpoints = self.breakpoints.get_mut(abs_path)?;            for (breakpoint, state) in it {                if let Some(to_update) = breakpoints                    .breakpoints                    .iter_mut()                    .find(|bp| *bp.position() == breakpoint.position)                {                    to_update                        .session_state                        .entry(session_id)                        .insert_entry(state);                }            }            Some(())        });    }     pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {        worktree::File::from_dyn(buffer.read(cx).file())            .map(|file| file.worktree.read(cx).absolutize(&file.path))            .map(Arc::<Path>::from)    }     pub fn toggle_breakpoint(        &mut self,        buffer: Entity<Buffer>,        mut breakpoint: BreakpointWithPosition,        edit_action: BreakpointEditAction,        cx: &mut Context<Self>,    ) {        let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {            return;        };         let breakpoint_set = self            .breakpoints            .entry(abs_path.clone())            .or_insert_with(|| BreakpointsInFile::new(buffer.clone(), cx));         // Buffers changed for the file, migrate breakpoints to the new buffer        if breakpoint_set.buffer != buffer {            let old_snapshot = breakpoint_set.buffer.read(cx).snapshot();            let new_snapshot = buffer.read(cx).snapshot();            let breakpoints = breakpoint_set                .breakpoints                .drain(..)                .map(|mut breakpoint| {                    let old_position =                        old_snapshot.summary_for_anchor::<PointUtf16>(breakpoint.position());                    let new_position = PointUtf16::new(old_position.row, 0);                    let new_position =                        new_snapshot.clip_point_utf16(Unclipped(new_position), Bias::Left);                    breakpoint.bp.position = new_snapshot.anchor_after(new_position);                    breakpoint                })                .collect();            *breakpoint_set = BreakpointsInFile::new(buffer, cx);            breakpoint_set.breakpoints = breakpoints;        }         match edit_action {            BreakpointEditAction::Toggle => {                let len_before = breakpoint_set.breakpoints.len();                breakpoint_set                    .breakpoints                    .retain(|value| breakpoint != value.bp);                if len_before == breakpoint_set.breakpoints.len() {                    // We did not remove any breakpoint, hence let's toggle one.                    breakpoint_set                        .breakpoints                        .push(StatefulBreakpoint::new(breakpoint.clone()));                }            }            BreakpointEditAction::InvertState => {                if let Some(bp) = breakpoint_set                    .breakpoints                    .iter_mut()                    .find(|value| breakpoint == value.bp)                {                    let bp = &mut bp.bp.bp;                    if bp.is_enabled() {                        bp.state = BreakpointState::Disabled;                    } else {                        bp.state = BreakpointState::Enabled;                    }                } else {                    breakpoint.bp.state = BreakpointState::Disabled;                    breakpoint_set                        .breakpoints                        .push(StatefulBreakpoint::new(breakpoint.clone()));                }            }            BreakpointEditAction::EditLogMessage(log_message) => {                if !log_message.is_empty() {                    let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| {                        if breakpoint.position == *bp.position() {                            Some(&mut bp.bp.bp)                        } else {                            None                        }                    });                     if let Some(found_bp) = found_bp {                        found_bp.message = Some(log_message);                    } else {                        breakpoint.bp.message = Some(log_message);                        // We did not remove any breakpoint, hence let's toggle one.                        breakpoint_set                            .breakpoints                            .push(StatefulBreakpoint::new(breakpoint.clone()));                    }                } else if breakpoint.bp.message.is_some() {                    if let Some(position) = breakpoint_set                        .breakpoints                        .iter()                        .find_position(|other| breakpoint == other.bp)                        .map(|res| res.0)                    {                        breakpoint_set.breakpoints.remove(position);                    } else {

Lines 1–500 of 1082.