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::{Result, anyhow};
5use breakpoints_in_file::BreakpointsInFile;
6use collections::BTreeMap;
7use dap::client::SessionId;
8use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task};
9use language::{Buffer, BufferSnapshot, proto::serialize_anchor as serialize_text_anchor};
10use rpc::{
11 AnyProtoClient, TypedEnvelope,
12 proto::{self},
13};
14use std::{hash::Hash, ops::Range, path::Path, sync::Arc};
15use text::{Point, PointUtf16};
16
17use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore};
18
19mod breakpoints_in_file {
20 use language::BufferEvent;
21
22 use super::*;
23
24 #[derive(Clone)]
25 pub(super) struct BreakpointsInFile {
26 pub(super) buffer: Entity<Buffer>,
27 // 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
28 pub(super) breakpoints: Vec<(text::Anchor, Breakpoint)>,
29 _subscription: Arc<Subscription>,
30 }
31
32 impl BreakpointsInFile {
33 pub(super) fn new(buffer: Entity<Buffer>, cx: &mut Context<BreakpointStore>) -> Self {
34 let subscription =
35 Arc::from(cx.subscribe(&buffer, |_, buffer, event, cx| match event {
36 BufferEvent::Saved => {
37 if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) {
38 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
39 abs_path,
40 BreakpointUpdatedReason::FileSaved,
41 ));
42 }
43 }
44 _ => {}
45 }));
46
47 BreakpointsInFile {
48 buffer,
49 breakpoints: Vec::new(),
50 _subscription: subscription,
51 }
52 }
53 }
54}
55
56#[derive(Clone)]
57struct RemoteBreakpointStore {
58 upstream_client: AnyProtoClient,
59 _upstream_project_id: u64,
60}
61
62#[derive(Clone)]
63struct LocalBreakpointStore {
64 worktree_store: Entity<WorktreeStore>,
65 buffer_store: Entity<BufferStore>,
66}
67
68#[derive(Clone)]
69enum BreakpointStoreMode {
70 Local(LocalBreakpointStore),
71 Remote(RemoteBreakpointStore),
72}
73pub struct BreakpointStore {
74 breakpoints: BTreeMap<Arc<Path>, BreakpointsInFile>,
75 downstream_client: Option<(AnyProtoClient, u64)>,
76 active_stack_frame: Option<(SessionId, Arc<Path>, text::Anchor)>,
77 // E.g ssh
78 mode: BreakpointStoreMode,
79}
80
81impl BreakpointStore {
82 pub fn init(client: &AnyProtoClient) {
83 client.add_entity_request_handler(Self::handle_toggle_breakpoint);
84 client.add_entity_message_handler(Self::handle_breakpoints_for_file);
85 }
86 pub fn local(worktree_store: Entity<WorktreeStore>, buffer_store: Entity<BufferStore>) -> Self {
87 BreakpointStore {
88 breakpoints: BTreeMap::new(),
89 mode: BreakpointStoreMode::Local(LocalBreakpointStore {
90 worktree_store,
91 buffer_store,
92 }),
93 downstream_client: None,
94 active_stack_frame: Default::default(),
95 }
96 }
97
98 pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self {
99 BreakpointStore {
100 breakpoints: BTreeMap::new(),
101 mode: BreakpointStoreMode::Remote(RemoteBreakpointStore {
102 upstream_client,
103 _upstream_project_id: upstream_project_id,
104 }),
105 downstream_client: None,
106 active_stack_frame: Default::default(),
107 }
108 }
109
110 pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) {
111 self.downstream_client = Some((downstream_client.clone(), project_id));
112 }
113
114 pub(crate) fn unshared(&mut self, cx: &mut Context<Self>) {
115 self.downstream_client.take();
116
117 cx.notify();
118 }
119
120 async fn handle_breakpoints_for_file(
121 this: Entity<Project>,
122 message: TypedEnvelope<proto::BreakpointsForFile>,
123 mut cx: AsyncApp,
124 ) -> Result<()> {
125 let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?;
126 if message.payload.breakpoints.is_empty() {
127 return Ok(());
128 }
129
130 let buffer = this
131 .update(&mut cx, |this, cx| {
132 let path =
133 this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?;
134 Some(this.open_buffer(path, cx))
135 })
136 .ok()
137 .flatten()
138 .ok_or_else(|| anyhow!("Invalid project path"))?
139 .await?;
140
141 breakpoints.update(&mut cx, move |this, cx| {
142 let bps = this
143 .breakpoints
144 .entry(Arc::<Path>::from(message.payload.path.as_ref()))
145 .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
146
147 bps.breakpoints = message
148 .payload
149 .breakpoints
150 .into_iter()
151 .filter_map(|breakpoint| {
152 let anchor = language::proto::deserialize_anchor(breakpoint.position.clone()?)?;
153 let breakpoint = Breakpoint::from_proto(breakpoint)?;
154 Some((anchor, breakpoint))
155 })
156 .collect();
157
158 cx.notify();
159 })?;
160
161 Ok(())
162 }
163
164 async fn handle_toggle_breakpoint(
165 this: Entity<Project>,
166 message: TypedEnvelope<proto::ToggleBreakpoint>,
167 mut cx: AsyncApp,
168 ) -> Result<proto::Ack> {
169 let breakpoints = this.update(&mut cx, |this, _| this.breakpoint_store())?;
170 let path = this
171 .update(&mut cx, |this, cx| {
172 this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)
173 })?
174 .ok_or_else(|| anyhow!("Could not resolve provided abs path"))?;
175 let buffer = this
176 .update(&mut cx, |this, cx| {
177 this.buffer_store().read(cx).get_by_path(&path, cx)
178 })?
179 .ok_or_else(|| anyhow!("Could not find buffer for a given path"))?;
180 let breakpoint = message
181 .payload
182 .breakpoint
183 .ok_or_else(|| anyhow!("Breakpoint not present in RPC payload"))?;
184 let anchor = language::proto::deserialize_anchor(
185 breakpoint
186 .position
187 .clone()
188 .ok_or_else(|| anyhow!("Anchor not present in RPC payload"))?,
189 )
190 .ok_or_else(|| anyhow!("Anchor deserialization failed"))?;
191 let breakpoint = Breakpoint::from_proto(breakpoint)
192 .ok_or_else(|| anyhow!("Could not deserialize breakpoint"))?;
193
194 breakpoints.update(&mut cx, |this, cx| {
195 this.toggle_breakpoint(
196 buffer,
197 (anchor, breakpoint),
198 BreakpointEditAction::Toggle,
199 cx,
200 );
201 })?;
202 Ok(proto::Ack {})
203 }
204
205 pub(crate) fn broadcast(&self) {
206 if let Some((client, project_id)) = &self.downstream_client {
207 for (path, breakpoint_set) in &self.breakpoints {
208 let _ = client.send(proto::BreakpointsForFile {
209 project_id: *project_id,
210 path: path.to_str().map(ToOwned::to_owned).unwrap(),
211 breakpoints: breakpoint_set
212 .breakpoints
213 .iter()
214 .filter_map(|(anchor, bp)| bp.to_proto(&path, anchor))
215 .collect(),
216 });
217 }
218 }
219 }
220
221 pub fn abs_path_from_buffer(buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
222 worktree::File::from_dyn(buffer.read(cx).file())
223 .and_then(|file| file.worktree.read(cx).absolutize(&file.path).ok())
224 .map(Arc::<Path>::from)
225 }
226
227 pub fn toggle_breakpoint(
228 &mut self,
229 buffer: Entity<Buffer>,
230 mut breakpoint: (text::Anchor, Breakpoint),
231 edit_action: BreakpointEditAction,
232 cx: &mut Context<Self>,
233 ) {
234 let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else {
235 return;
236 };
237
238 let breakpoint_set = self
239 .breakpoints
240 .entry(abs_path.clone())
241 .or_insert_with(|| BreakpointsInFile::new(buffer, cx));
242
243 match edit_action {
244 BreakpointEditAction::Toggle => {
245 let len_before = breakpoint_set.breakpoints.len();
246 breakpoint_set
247 .breakpoints
248 .retain(|value| &breakpoint != value);
249 if len_before == breakpoint_set.breakpoints.len() {
250 // We did not remove any breakpoint, hence let's toggle one.
251 breakpoint_set.breakpoints.push(breakpoint.clone());
252 }
253 }
254 BreakpointEditAction::InvertState => {
255 if let Some((_, bp)) = breakpoint_set
256 .breakpoints
257 .iter_mut()
258 .find(|value| breakpoint == **value)
259 {
260 if bp.is_enabled() {
261 bp.state = BreakpointState::Disabled;
262 } else {
263 bp.state = BreakpointState::Enabled;
264 }
265 } else {
266 breakpoint.1.state = BreakpointState::Disabled;
267 breakpoint_set.breakpoints.push(breakpoint.clone());
268 }
269 }
270 BreakpointEditAction::EditLogMessage(log_message) => {
271 if !log_message.is_empty() {
272 let found_bp =
273 breakpoint_set
274 .breakpoints
275 .iter_mut()
276 .find_map(|(other_pos, other_bp)| {
277 if breakpoint.0 == *other_pos {
278 Some(other_bp)
279 } else {
280 None
281 }
282 });
283
284 if let Some(found_bp) = found_bp {
285 found_bp.message = Some(log_message.clone());
286 } else {
287 breakpoint.1.message = Some(log_message.clone());
288 // We did not remove any breakpoint, hence let's toggle one.
289 breakpoint_set.breakpoints.push(breakpoint.clone());
290 }
291 } else if breakpoint.1.message.is_some() {
292 breakpoint_set.breakpoints.retain(|(other_pos, other)| {
293 &breakpoint.0 != other_pos && other.message.is_none()
294 })
295 }
296 }
297 BreakpointEditAction::EditHitCondition(hit_condition) => {
298 if !hit_condition.is_empty() {
299 let found_bp =
300 breakpoint_set
301 .breakpoints
302 .iter_mut()
303 .find_map(|(other_pos, other_bp)| {
304 if breakpoint.0 == *other_pos {
305 Some(other_bp)
306 } else {
307 None
308 }
309 });
310
311 if let Some(found_bp) = found_bp {
312 found_bp.hit_condition = Some(hit_condition.clone());
313 } else {
314 breakpoint.1.hit_condition = Some(hit_condition.clone());
315 // We did not remove any breakpoint, hence let's toggle one.
316 breakpoint_set.breakpoints.push(breakpoint.clone());
317 }
318 } else if breakpoint.1.hit_condition.is_some() {
319 breakpoint_set.breakpoints.retain(|(other_pos, other)| {
320 &breakpoint.0 != other_pos && other.hit_condition.is_none()
321 })
322 }
323 }
324 BreakpointEditAction::EditCondition(condition) => {
325 if !condition.is_empty() {
326 let found_bp =
327 breakpoint_set
328 .breakpoints
329 .iter_mut()
330 .find_map(|(other_pos, other_bp)| {
331 if breakpoint.0 == *other_pos {
332 Some(other_bp)
333 } else {
334 None
335 }
336 });
337
338 if let Some(found_bp) = found_bp {
339 found_bp.condition = Some(condition.clone());
340 } else {
341 breakpoint.1.condition = Some(condition.clone());
342 // We did not remove any breakpoint, hence let's toggle one.
343 breakpoint_set.breakpoints.push(breakpoint.clone());
344 }
345 } else if breakpoint.1.condition.is_some() {
346 breakpoint_set.breakpoints.retain(|(other_pos, other)| {
347 &breakpoint.0 != other_pos && other.condition.is_none()
348 })
349 }
350 }
351 }
352
353 if breakpoint_set.breakpoints.is_empty() {
354 self.breakpoints.remove(&abs_path);
355 }
356 if let BreakpointStoreMode::Remote(remote) = &self.mode {
357 if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
358 cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
359 project_id: remote._upstream_project_id,
360 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
361 breakpoint: Some(breakpoint),
362 }))
363 .detach();
364 }
365 } else if let Some((client, project_id)) = &self.downstream_client {
366 let breakpoints = self
367 .breakpoints
368 .get(&abs_path)
369 .map(|breakpoint_set| {
370 breakpoint_set
371 .breakpoints
372 .iter()
373 .filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
374 .collect()
375 })
376 .unwrap_or_default();
377
378 let _ = client.send(proto::BreakpointsForFile {
379 project_id: *project_id,
380 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
381 breakpoints,
382 });
383 }
384
385 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
386 abs_path,
387 BreakpointUpdatedReason::Toggled,
388 ));
389 cx.notify();
390 }
391
392 pub fn on_file_rename(
393 &mut self,
394 old_path: Arc<Path>,
395 new_path: Arc<Path>,
396 cx: &mut Context<Self>,
397 ) {
398 if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
399 self.breakpoints.insert(new_path.clone(), breakpoints);
400
401 cx.notify();
402 }
403 }
404
405 pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
406 let breakpoint_paths = self.breakpoints.keys().cloned().collect();
407 self.breakpoints.clear();
408 cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
409 }
410
411 pub fn breakpoints<'a>(
412 &'a self,
413 buffer: &'a Entity<Buffer>,
414 range: Option<Range<text::Anchor>>,
415 buffer_snapshot: &'a BufferSnapshot,
416 cx: &App,
417 ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
418 let abs_path = Self::abs_path_from_buffer(buffer, cx);
419 abs_path
420 .and_then(|path| self.breakpoints.get(&path))
421 .into_iter()
422 .flat_map(move |file_breakpoints| {
423 file_breakpoints.breakpoints.iter().filter({
424 let range = range.clone();
425 move |(position, _)| {
426 if let Some(range) = &range {
427 position.cmp(&range.start, buffer_snapshot).is_ge()
428 && position.cmp(&range.end, buffer_snapshot).is_le()
429 } else {
430 true
431 }
432 }
433 })
434 })
435 }
436
437 pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, text::Anchor)> {
438 self.active_stack_frame.as_ref()
439 }
440
441 pub fn remove_active_position(
442 &mut self,
443 session_id: Option<SessionId>,
444 cx: &mut Context<Self>,
445 ) {
446 if let Some(session_id) = session_id {
447 self.active_stack_frame
448 .take_if(|(id, _, _)| *id == session_id);
449 } else {
450 self.active_stack_frame.take();
451 }
452
453 cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
454 cx.notify();
455 }
456
457 pub fn set_active_position(
458 &mut self,
459 position: (SessionId, Arc<Path>, text::Anchor),
460 cx: &mut Context<Self>,
461 ) {
462 self.active_stack_frame = Some(position);
463 cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
464 cx.notify();
465 }
466
467 pub fn breakpoint_at_row(
468 &self,
469 path: &Path,
470 row: u32,
471 cx: &App,
472 ) -> Option<(Entity<Buffer>, (text::Anchor, Breakpoint))> {
473 self.breakpoints.get(path).and_then(|breakpoints| {
474 let snapshot = breakpoints.buffer.read(cx).text_snapshot();
475
476 breakpoints
477 .breakpoints
478 .iter()
479 .find(|(anchor, _)| anchor.summary::<Point>(&snapshot).row == row)
480 .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone()))
481 })
482 }
483
484 pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
485 self.breakpoints
486 .get(path)
487 .map(|bp| {
488 let snapshot = bp.buffer.read(cx).snapshot();
489 bp.breakpoints
490 .iter()
491 .map(|(position, breakpoint)| {
492 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
493 SourceBreakpoint {
494 row: position,
495 path: path.clone(),
496 state: breakpoint.state,
497 message: breakpoint.message.clone(),
498 condition: breakpoint.condition.clone(),
499 hit_condition: breakpoint.hit_condition.clone(),
500 }
501 })
502 .collect()
503 })
504 .unwrap_or_default()
505 }
506
507 pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
508 self.breakpoints
509 .iter()
510 .map(|(path, bp)| {
511 let snapshot = bp.buffer.read(cx).snapshot();
512 (
513 path.clone(),
514 bp.breakpoints
515 .iter()
516 .map(|(position, breakpoint)| {
517 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
518 SourceBreakpoint {
519 row: position,
520 path: path.clone(),
521 message: breakpoint.message.clone(),
522 state: breakpoint.state,
523 hit_condition: breakpoint.hit_condition.clone(),
524 condition: breakpoint.condition.clone(),
525 }
526 })
527 .collect(),
528 )
529 })
530 .collect()
531 }
532
533 pub fn with_serialized_breakpoints(
534 &self,
535 breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
536 cx: &mut Context<BreakpointStore>,
537 ) -> Task<Result<()>> {
538 if let BreakpointStoreMode::Local(mode) = &self.mode {
539 let mode = mode.clone();
540 cx.spawn(async move |this, cx| {
541 let mut new_breakpoints = BTreeMap::default();
542 for (path, bps) in breakpoints {
543 if bps.is_empty() {
544 continue;
545 }
546 let (worktree, relative_path) = mode
547 .worktree_store
548 .update(cx, |this, cx| {
549 this.find_or_create_worktree(&path, false, cx)
550 })?
551 .await?;
552 let buffer = mode
553 .buffer_store
554 .update(cx, |this, cx| {
555 let path = ProjectPath {
556 worktree_id: worktree.read(cx).id(),
557 path: relative_path.into(),
558 };
559 this.open_buffer(path, cx)
560 })?
561 .await;
562 let Ok(buffer) = buffer else {
563 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
564 continue;
565 };
566 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
567
568 let mut breakpoints_for_file =
569 this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
570
571 for bp in bps {
572 let position = snapshot.anchor_after(PointUtf16::new(bp.row, 0));
573 breakpoints_for_file.breakpoints.push((
574 position,
575 Breakpoint {
576 message: bp.message,
577 state: bp.state,
578 condition: bp.condition,
579 hit_condition: bp.hit_condition,
580 },
581 ))
582 }
583 new_breakpoints.insert(path, breakpoints_for_file);
584 }
585 this.update(cx, |this, cx| {
586 log::info!("Finish deserializing breakpoints & initializing breakpoint store");
587 for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
588 (path.to_string_lossy(), bp_in_file.breakpoints.len())
589 }) {
590 let breakpoint_str = if count > 1 {
591 "breakpoints"
592 } else {
593 "breakpoint"
594 };
595 log::info!("Deserialized {count} {breakpoint_str} at path: {path}");
596 }
597
598 this.breakpoints = new_breakpoints;
599
600 cx.notify();
601 })?;
602
603 Ok(())
604 })
605 } else {
606 Task::ready(Ok(()))
607 }
608 }
609
610 #[cfg(any(test, feature = "test-support"))]
611 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
612 self.breakpoints.keys().cloned().collect()
613 }
614}
615
616#[derive(Clone, Copy)]
617pub enum BreakpointUpdatedReason {
618 Toggled,
619 FileSaved,
620}
621
622pub enum BreakpointStoreEvent {
623 ActiveDebugLineChanged,
624 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
625 BreakpointsCleared(Vec<Arc<Path>>),
626}
627
628impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
629
630type BreakpointMessage = Arc<str>;
631
632#[derive(Clone, Debug)]
633pub enum BreakpointEditAction {
634 Toggle,
635 InvertState,
636 EditLogMessage(BreakpointMessage),
637 EditCondition(BreakpointMessage),
638 EditHitCondition(BreakpointMessage),
639}
640
641#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
642pub enum BreakpointState {
643 Enabled,
644 Disabled,
645}
646
647impl BreakpointState {
648 #[inline]
649 pub fn is_enabled(&self) -> bool {
650 matches!(self, BreakpointState::Enabled)
651 }
652
653 #[inline]
654 pub fn is_disabled(&self) -> bool {
655 matches!(self, BreakpointState::Disabled)
656 }
657
658 #[inline]
659 pub fn to_int(&self) -> i32 {
660 match self {
661 BreakpointState::Enabled => 0,
662 BreakpointState::Disabled => 1,
663 }
664 }
665}
666
667#[derive(Clone, Debug, Hash, PartialEq, Eq)]
668pub struct Breakpoint {
669 pub message: Option<BreakpointMessage>,
670 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
671 pub hit_condition: Option<BreakpointMessage>,
672 pub condition: Option<BreakpointMessage>,
673 pub state: BreakpointState,
674}
675
676impl Breakpoint {
677 pub fn new_standard() -> Self {
678 Self {
679 state: BreakpointState::Enabled,
680 hit_condition: None,
681 condition: None,
682 message: None,
683 }
684 }
685
686 pub fn new_condition(hit_condition: &str) -> Self {
687 Self {
688 state: BreakpointState::Enabled,
689 condition: None,
690 hit_condition: Some(hit_condition.into()),
691 message: None,
692 }
693 }
694
695 pub fn new_log(log_message: &str) -> Self {
696 Self {
697 state: BreakpointState::Enabled,
698 hit_condition: None,
699 condition: None,
700 message: Some(log_message.into()),
701 }
702 }
703
704 fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
705 Some(client::proto::Breakpoint {
706 position: Some(serialize_text_anchor(position)),
707 state: match self.state {
708 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
709 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
710 },
711 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
712 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
713 hit_condition: self
714 .hit_condition
715 .as_ref()
716 .map(|s| String::from(s.as_ref())),
717 })
718 }
719
720 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
721 Some(Self {
722 state: match proto::BreakpointState::from_i32(breakpoint.state) {
723 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
724 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
725 },
726 message: breakpoint.message.map(Into::into),
727 condition: breakpoint.condition.map(Into::into),
728 hit_condition: breakpoint.hit_condition.map(Into::into),
729 })
730 }
731
732 #[inline]
733 pub fn is_enabled(&self) -> bool {
734 self.state.is_enabled()
735 }
736
737 #[inline]
738 pub fn is_disabled(&self) -> bool {
739 self.state.is_disabled()
740 }
741}
742
743/// Breakpoint for location within source code.
744#[derive(Clone, Debug, Hash, PartialEq, Eq)]
745pub struct SourceBreakpoint {
746 pub row: u32,
747 pub path: Arc<Path>,
748 pub message: Option<Arc<str>>,
749 pub condition: Option<Arc<str>>,
750 pub hit_condition: Option<Arc<str>>,
751 pub state: BreakpointState,
752}
753
754impl From<SourceBreakpoint> for dap::SourceBreakpoint {
755 fn from(bp: SourceBreakpoint) -> Self {
756 Self {
757 line: bp.row as u64 + 1,
758 column: None,
759 condition: bp
760 .condition
761 .map(|condition| String::from(condition.as_ref())),
762 hit_condition: bp
763 .hit_condition
764 .map(|hit_condition| String::from(hit_condition.as_ref())),
765 log_message: bp.message.map(|message| String::from(message.as_ref())),
766 mode: None,
767 }
768 }
769}