Closes https://github.com/zed-industries/zed/issues/38690 Closes #37353 ### Background On Windows, paths are normally separated by `\`, unlike mac and linux where they are separated by `/`. When editing code in a project that uses a different path style than your local system (e.g. remoting from Windows to Linux, using WSL, and collaboration between windows and unix users), the correct separator for a path may differ from the "native" separator. Previously, to work around this, Zed converted paths' separators in numerous places. This was applied to both absolute and relative paths, leading to incorrect conversions in some cases. ### Solution Many code paths in Zed use paths that are *relative* to either a worktree root or a git repository. This PR introduces a dedicated type for these paths called `RelPath`, which stores the path in the same way regardless of host platform, and offers `Path`-like manipulation APIs. RelPath supports *displaying* the path using either separator, so that we can display paths in a style that is determined at runtime based on the current project. The representation of absolute paths is left untouched, for now. Absolute paths are different from relative paths because (except in contexts where we know that the path refers to the local filesystem) they should generally be treated as opaque strings. Currently we use a mix of types for these paths (std::path::Path, String, SanitizedPath). Release Notes: - N/A --------- Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Peter Tripp <petertripp@gmail.com> Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com> Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
1035 lines
37 KiB
Rust
1035 lines
37 KiB
Rust
//! 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, 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::{Point, PointUtf16};
|
|
use util::maybe;
|
|
|
|
use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
|
|
|
|
use super::session::ThreadId;
|
|
|
|
mod breakpoints_in_file {
|
|
use collections::HashMap;
|
|
use language::{BufferEvent, DiskState};
|
|
|
|
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() == DiskState::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)]
|
|
struct LocalBreakpointStore {
|
|
worktree_store: Entity<WorktreeStore>,
|
|
buffer_store: Entity<BufferStore>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
enum BreakpointStoreMode {
|
|
Local(LocalBreakpointStore),
|
|
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 {
|
|
breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,
|
|
downstream_client: Option<(AnyProtoClient, u64)>,
|
|
active_stack_frame: Option<ActiveStackFrame>,
|
|
// 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(LocalBreakpointStore {
|
|
worktree_store,
|
|
buffer_store,
|
|
}),
|
|
downstream_client: None,
|
|
active_stack_frame: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
|
|
BreakpointStore {
|
|
breakpoints: BTreeMap::new(),
|
|
mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
|
|
upstream_client,
|
|
_upstream_project_id: upstream_project_id,
|
|
}),
|
|
downstream_client: None,
|
|
active_stack_frame: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub(crate) 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<Project>,
|
|
message: TypedEnvelope<proto::BreakpointsForFile>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<()> {
|
|
let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
|
|
if message.payload.breakpoints.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let buffer = this
|
|
.update(&mut cx, |this, cx| {
|
|
let path =
|
|
this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
|
|
Some(this.open_buffer(path, cx))
|
|
})
|
|
.ok()
|
|
.flatten()
|
|
.context("Invalid project path")?
|
|
.await?;
|
|
|
|
breakpoints.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<Project>,
|
|
message: TypedEnvelope<proto::ToggleBreakpoint>,
|
|
mut cx: AsyncApp,
|
|
) -> Result<proto::Ack> {
|
|
let breakpoints = this.read_with(&cx, |this, _| this.breakpoint_store())?;
|
|
let path = this
|
|
.update(&mut cx, |this, cx| {
|
|
this.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")?;
|
|
|
|
breakpoints.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_str().map(ToOwned::to_owned).unwrap(),
|
|
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, cx));
|
|
|
|
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 {
|
|
log::error!("Failed to find position of breakpoint to delete")
|
|
}
|
|
}
|
|
}
|
|
BreakpointEditAction::EditHitCondition(hit_condition) => {
|
|
if !hit_condition.is_empty() {
|
|
let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
|
|
if breakpoint.position == *other.position() {
|
|
Some(&mut other.bp.bp)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
if let Some(found_bp) = found_bp {
|
|
found_bp.hit_condition = Some(hit_condition);
|
|
} else {
|
|
breakpoint.bp.hit_condition = Some(hit_condition);
|
|
// We did not remove any breakpoint, hence let's toggle one.
|
|
breakpoint_set
|
|
.breakpoints
|
|
.push(StatefulBreakpoint::new(breakpoint.clone()))
|
|
}
|
|
} else if breakpoint.bp.hit_condition.is_some() {
|
|
if let Some(position) = breakpoint_set
|
|
.breakpoints
|
|
.iter()
|
|
.find_position(|bp| breakpoint == bp.bp)
|
|
.map(|res| res.0)
|
|
{
|
|
breakpoint_set.breakpoints.remove(position);
|
|
} else {
|
|
log::error!("Failed to find position of breakpoint to delete")
|
|
}
|
|
}
|
|
}
|
|
BreakpointEditAction::EditCondition(condition) => {
|
|
if !condition.is_empty() {
|
|
let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| {
|
|
if breakpoint.position == *other.position() {
|
|
Some(&mut other.bp.bp)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
|
|
if let Some(found_bp) = found_bp {
|
|
found_bp.condition = Some(condition);
|
|
} else {
|
|
breakpoint.bp.condition = Some(condition);
|
|
// We did not remove any breakpoint, hence let's toggle one.
|
|
breakpoint_set
|
|
.breakpoints
|
|
.push(StatefulBreakpoint::new(breakpoint.clone()));
|
|
}
|
|
} else if breakpoint.bp.condition.is_some() {
|
|
if let Some(position) = breakpoint_set
|
|
.breakpoints
|
|
.iter()
|
|
.find_position(|bp| breakpoint == bp.bp)
|
|
.map(|res| res.0)
|
|
{
|
|
breakpoint_set.breakpoints.remove(position);
|
|
} else {
|
|
log::error!("Failed to find position of breakpoint to delete")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if breakpoint_set.breakpoints.is_empty() {
|
|
self.breakpoints.remove(&abs_path);
|
|
}
|
|
if let BreakpointStoreMode::Remote(remote) = &self.mode {
|
|
if let Some(breakpoint) =
|
|
breakpoint
|
|
.bp
|
|
.to_proto(&abs_path, &breakpoint.position, &HashMap::default())
|
|
{
|
|
cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
|
|
project_id: remote._upstream_project_id,
|
|
path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
|
|
breakpoint: Some(breakpoint),
|
|
}))
|
|
.detach();
|
|
}
|
|
} else if let Some((client, project_id)) = &self.downstream_client {
|
|
let breakpoints = self
|
|
.breakpoints
|
|
.get(&abs_path)
|
|
.map(|breakpoint_set| {
|
|
breakpoint_set
|
|
.breakpoints
|
|
.iter()
|
|
.filter_map(|bp| {
|
|
bp.bp
|
|
.bp
|
|
.to_proto(&abs_path, bp.position(), &bp.session_state)
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let _ = client.send(proto::BreakpointsForFile {
|
|
project_id: *project_id,
|
|
path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
|
|
breakpoints,
|
|
});
|
|
}
|
|
|
|
cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
|
|
abs_path,
|
|
BreakpointUpdatedReason::Toggled,
|
|
));
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn on_file_rename(
|
|
&mut self,
|
|
old_path: Arc<Path>,
|
|
new_path: Arc<Path>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
|
|
self.breakpoints.insert(new_path, breakpoints);
|
|
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
|
|
let breakpoint_paths = self.breakpoints.keys().cloned().collect();
|
|
self.breakpoints.clear();
|
|
cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
|
|
}
|
|
|
|
pub fn breakpoints<'a>(
|
|
&'a self,
|
|
buffer: &'a Entity<Buffer>,
|
|
range: Option<Range<text::Anchor>>,
|
|
buffer_snapshot: &'a BufferSnapshot,
|
|
cx: &App,
|
|
) -> impl Iterator<Item = (&'a BreakpointWithPosition, Option<BreakpointSessionState>)> + 'a
|
|
{
|
|
let abs_path = Self::abs_path_from_buffer(buffer, cx);
|
|
let active_session_id = self
|
|
.active_stack_frame
|
|
.as_ref()
|
|
.map(|frame| frame.session_id);
|
|
abs_path
|
|
.and_then(|path| self.breakpoints.get(&path))
|
|
.into_iter()
|
|
.flat_map(move |file_breakpoints| {
|
|
file_breakpoints.breakpoints.iter().filter_map({
|
|
let range = range.clone();
|
|
move |bp| {
|
|
if let Some(range) = &range
|
|
&& (bp.position().cmp(&range.start, buffer_snapshot).is_lt()
|
|
|| bp.position().cmp(&range.end, buffer_snapshot).is_gt())
|
|
{
|
|
return None;
|
|
}
|
|
let session_state = active_session_id
|
|
.and_then(|id| bp.session_state.get(&id))
|
|
.copied();
|
|
Some((&bp.bp, session_state))
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn active_position(&self) -> Option<&ActiveStackFrame> {
|
|
self.active_stack_frame.as_ref()
|
|
}
|
|
|
|
pub fn remove_active_position(
|
|
&mut self,
|
|
session_id: Option<SessionId>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
if let Some(session_id) = session_id {
|
|
self.active_stack_frame
|
|
.take_if(|active_stack_frame| active_stack_frame.session_id == session_id);
|
|
} else {
|
|
self.active_stack_frame.take();
|
|
}
|
|
|
|
cx.emit(BreakpointStoreEvent::ClearDebugLines);
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context<Self>) {
|
|
if self
|
|
.active_stack_frame
|
|
.as_ref()
|
|
.is_some_and(|active_position| active_position == &position)
|
|
{
|
|
cx.emit(BreakpointStoreEvent::SetDebugLine);
|
|
return;
|
|
}
|
|
|
|
if self.active_stack_frame.is_some() {
|
|
cx.emit(BreakpointStoreEvent::ClearDebugLines);
|
|
}
|
|
|
|
self.active_stack_frame = Some(position);
|
|
|
|
cx.emit(BreakpointStoreEvent::SetDebugLine);
|
|
cx.notify();
|
|
}
|
|
|
|
pub fn breakpoint_at_row(
|
|
&self,
|
|
path: &Path,
|
|
row: u32,
|
|
cx: &App,
|
|
) -> Option<(Entity<Buffer>, BreakpointWithPosition)> {
|
|
self.breakpoints.get(path).and_then(|breakpoints| {
|
|
let snapshot = breakpoints.buffer.read(cx).text_snapshot();
|
|
|
|
breakpoints
|
|
.breakpoints
|
|
.iter()
|
|
.find(|bp| bp.position().summary::<Point>(&snapshot).row == row)
|
|
.map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone()))
|
|
})
|
|
}
|
|
|
|
pub fn breakpoints_from_path(&self, path: &Arc<Path>) -> Vec<BreakpointWithPosition> {
|
|
self.breakpoints
|
|
.get(path)
|
|
.map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn source_breakpoints_from_path(
|
|
&self,
|
|
path: &Arc<Path>,
|
|
cx: &App,
|
|
) -> Vec<SourceBreakpoint> {
|
|
self.breakpoints
|
|
.get(path)
|
|
.map(|bp| {
|
|
let snapshot = bp.buffer.read(cx).snapshot();
|
|
bp.breakpoints
|
|
.iter()
|
|
.map(|bp| {
|
|
let position = snapshot.summary_for_anchor::<PointUtf16>(bp.position()).row;
|
|
let bp = &bp.bp;
|
|
SourceBreakpoint {
|
|
row: position,
|
|
path: path.clone(),
|
|
state: bp.bp.state,
|
|
message: bp.bp.message.clone(),
|
|
condition: bp.bp.condition.clone(),
|
|
hit_condition: bp.bp.hit_condition.clone(),
|
|
}
|
|
})
|
|
.collect()
|
|
})
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn all_breakpoints(&self) -> BTreeMap<Arc<Path>, Vec<BreakpointWithPosition>> {
|
|
self.breakpoints
|
|
.iter()
|
|
.map(|(path, bp)| {
|
|
(
|
|
path.clone(),
|
|
bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
|
|
self.breakpoints
|
|
.iter()
|
|
.map(|(path, bp)| {
|
|
let snapshot = bp.buffer.read(cx).snapshot();
|
|
(
|
|
path.clone(),
|
|
bp.breakpoints
|
|
.iter()
|
|
.map(|breakpoint| {
|
|
let position = snapshot
|
|
.summary_for_anchor::<PointUtf16>(breakpoint.position())
|
|
.row;
|
|
let breakpoint = &breakpoint.bp;
|
|
SourceBreakpoint {
|
|
row: position,
|
|
path: path.clone(),
|
|
message: breakpoint.bp.message.clone(),
|
|
state: breakpoint.bp.state,
|
|
hit_condition: breakpoint.bp.hit_condition.clone(),
|
|
condition: breakpoint.bp.condition.clone(),
|
|
}
|
|
})
|
|
.collect(),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn with_serialized_breakpoints(
|
|
&self,
|
|
breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
|
|
cx: &mut Context<BreakpointStore>,
|
|
) -> Task<Result<()>> {
|
|
if let BreakpointStoreMode::Local(mode) = &self.mode {
|
|
let mode = mode.clone();
|
|
cx.spawn(async move |this, cx| {
|
|
let mut new_breakpoints = BTreeMap::default();
|
|
for (path, bps) in breakpoints {
|
|
if bps.is_empty() {
|
|
continue;
|
|
}
|
|
let (worktree, relative_path) = mode
|
|
.worktree_store
|
|
.update(cx, |this, cx| {
|
|
this.find_or_create_worktree(&path, false, cx)
|
|
})?
|
|
.await?;
|
|
let buffer = mode
|
|
.buffer_store
|
|
.update(cx, |this, cx| {
|
|
let path = ProjectPath {
|
|
worktree_id: worktree.read(cx).id(),
|
|
path: relative_path,
|
|
};
|
|
this.open_buffer(path, cx)
|
|
})?
|
|
.await;
|
|
let Ok(buffer) = buffer else {
|
|
log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
|
|
continue;
|
|
};
|
|
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
|
|
|
let mut breakpoints_for_file =
|
|
this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
|
|
|
|
for bp in bps {
|
|
let max_point = snapshot.max_point_utf16();
|
|
let point = PointUtf16::new(bp.row, 0);
|
|
if point > max_point {
|
|
log::error!("skipping a deserialized breakpoint that's out of range");
|
|
continue;
|
|
}
|
|
let position = snapshot.anchor_after(point);
|
|
breakpoints_for_file
|
|
.breakpoints
|
|
.push(StatefulBreakpoint::new(BreakpointWithPosition {
|
|
position,
|
|
bp: Breakpoint {
|
|
message: bp.message,
|
|
state: bp.state,
|
|
condition: bp.condition,
|
|
hit_condition: bp.hit_condition,
|
|
},
|
|
}))
|
|
}
|
|
new_breakpoints.insert(path, breakpoints_for_file);
|
|
}
|
|
this.update(cx, |this, cx| {
|
|
for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
|
|
(path.to_string_lossy(), bp_in_file.breakpoints.len())
|
|
}) {
|
|
let breakpoint_str = if count > 1 {
|
|
"breakpoints"
|
|
} else {
|
|
"breakpoint"
|
|
};
|
|
log::debug!("Deserialized {count} {breakpoint_str} at path: {path}");
|
|
}
|
|
|
|
this.breakpoints = new_breakpoints;
|
|
|
|
cx.notify();
|
|
})?;
|
|
|
|
Ok(())
|
|
})
|
|
} else {
|
|
Task::ready(Ok(()))
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
|
|
self.breakpoints.keys().cloned().collect()
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub enum BreakpointUpdatedReason {
|
|
Toggled,
|
|
FileSaved,
|
|
}
|
|
|
|
pub enum BreakpointStoreEvent {
|
|
SetDebugLine,
|
|
ClearDebugLines,
|
|
BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
|
|
BreakpointsCleared(Vec<Arc<Path>>),
|
|
}
|
|
|
|
impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
|
|
|
|
type BreakpointMessage = Arc<str>;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum BreakpointEditAction {
|
|
Toggle,
|
|
InvertState,
|
|
EditLogMessage(BreakpointMessage),
|
|
EditCondition(BreakpointMessage),
|
|
EditHitCondition(BreakpointMessage),
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
|
|
pub enum BreakpointState {
|
|
Enabled,
|
|
Disabled,
|
|
}
|
|
|
|
impl BreakpointState {
|
|
#[inline]
|
|
pub fn is_enabled(&self) -> bool {
|
|
matches!(self, BreakpointState::Enabled)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_disabled(&self) -> bool {
|
|
matches!(self, BreakpointState::Disabled)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn to_int(self) -> i32 {
|
|
match self {
|
|
BreakpointState::Enabled => 0,
|
|
BreakpointState::Disabled => 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
|
pub struct Breakpoint {
|
|
pub message: Option<BreakpointMessage>,
|
|
/// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
|
|
pub hit_condition: Option<Arc<str>>,
|
|
pub condition: Option<BreakpointMessage>,
|
|
pub state: BreakpointState,
|
|
}
|
|
|
|
impl Breakpoint {
|
|
pub fn new_standard() -> Self {
|
|
Self {
|
|
state: BreakpointState::Enabled,
|
|
hit_condition: None,
|
|
condition: None,
|
|
message: None,
|
|
}
|
|
}
|
|
|
|
pub fn new_condition(hit_condition: &str) -> Self {
|
|
Self {
|
|
state: BreakpointState::Enabled,
|
|
condition: None,
|
|
hit_condition: Some(hit_condition.into()),
|
|
message: None,
|
|
}
|
|
}
|
|
|
|
pub fn new_log(log_message: &str) -> Self {
|
|
Self {
|
|
state: BreakpointState::Enabled,
|
|
hit_condition: None,
|
|
condition: None,
|
|
message: Some(log_message.into()),
|
|
}
|
|
}
|
|
|
|
fn to_proto(
|
|
&self,
|
|
_path: &Path,
|
|
position: &text::Anchor,
|
|
session_states: &HashMap<SessionId, BreakpointSessionState>,
|
|
) -> Option<client::proto::Breakpoint> {
|
|
Some(client::proto::Breakpoint {
|
|
position: Some(serialize_text_anchor(position)),
|
|
state: match self.state {
|
|
BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
|
|
BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
|
|
},
|
|
message: self.message.as_ref().map(|s| String::from(s.as_ref())),
|
|
condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
|
|
hit_condition: self
|
|
.hit_condition
|
|
.as_ref()
|
|
.map(|s| String::from(s.as_ref())),
|
|
session_state: session_states
|
|
.iter()
|
|
.map(|(session_id, state)| {
|
|
(
|
|
session_id.to_proto(),
|
|
proto::BreakpointSessionState {
|
|
id: state.id,
|
|
verified: state.verified,
|
|
},
|
|
)
|
|
})
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
|
|
Some(Self {
|
|
state: match proto::BreakpointState::from_i32(breakpoint.state) {
|
|
Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
|
|
None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
|
|
},
|
|
message: breakpoint.message.map(Into::into),
|
|
condition: breakpoint.condition.map(Into::into),
|
|
hit_condition: breakpoint.hit_condition.map(Into::into),
|
|
})
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_enabled(&self) -> bool {
|
|
self.state.is_enabled()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn is_disabled(&self) -> bool {
|
|
self.state.is_disabled()
|
|
}
|
|
}
|
|
|
|
/// Breakpoint for location within source code.
|
|
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
|
pub struct SourceBreakpoint {
|
|
pub row: u32,
|
|
pub path: Arc<Path>,
|
|
pub message: Option<Arc<str>>,
|
|
pub condition: Option<Arc<str>>,
|
|
pub hit_condition: Option<Arc<str>>,
|
|
pub state: BreakpointState,
|
|
}
|
|
|
|
impl From<SourceBreakpoint> for dap::SourceBreakpoint {
|
|
fn from(bp: SourceBreakpoint) -> Self {
|
|
Self {
|
|
line: bp.row as u64 + 1,
|
|
column: None,
|
|
condition: bp
|
|
.condition
|
|
.map(|condition| String::from(condition.as_ref())),
|
|
hit_condition: bp
|
|
.hit_condition
|
|
.map(|hit_condition| String::from(hit_condition.as_ref())),
|
|
log_message: bp.message.map(|message| String::from(message.as_ref())),
|
|
mode: None,
|
|
}
|
|
}
|
|
}
|