//! 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::{Point, PointUtf16};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, 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 .breakpointsLines 1–500 of 1061. Load more
//! 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::{Point, PointUtf16}; 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, 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