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::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 breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
468 self.breakpoints
469 .get(path)
470 .map(|bp| {
471 let snapshot = bp.buffer.read(cx).snapshot();
472 bp.breakpoints
473 .iter()
474 .map(|(position, breakpoint)| {
475 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
476 SourceBreakpoint {
477 row: position,
478 path: path.clone(),
479 state: breakpoint.state,
480 message: breakpoint.message.clone(),
481 condition: breakpoint.condition.clone(),
482 hit_condition: breakpoint.hit_condition.clone(),
483 }
484 })
485 .collect()
486 })
487 .unwrap_or_default()
488 }
489
490 pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
491 self.breakpoints
492 .iter()
493 .map(|(path, bp)| {
494 let snapshot = bp.buffer.read(cx).snapshot();
495 (
496 path.clone(),
497 bp.breakpoints
498 .iter()
499 .map(|(position, breakpoint)| {
500 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
501 SourceBreakpoint {
502 row: position,
503 path: path.clone(),
504 message: breakpoint.message.clone(),
505 state: breakpoint.state,
506 hit_condition: breakpoint.hit_condition.clone(),
507 condition: breakpoint.condition.clone(),
508 }
509 })
510 .collect(),
511 )
512 })
513 .collect()
514 }
515
516 pub fn with_serialized_breakpoints(
517 &self,
518 breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
519 cx: &mut Context<BreakpointStore>,
520 ) -> Task<Result<()>> {
521 if let BreakpointStoreMode::Local(mode) = &self.mode {
522 let mode = mode.clone();
523 cx.spawn(async move |this, cx| {
524 let mut new_breakpoints = BTreeMap::default();
525 for (path, bps) in breakpoints {
526 if bps.is_empty() {
527 continue;
528 }
529 let (worktree, relative_path) = mode
530 .worktree_store
531 .update(cx, |this, cx| {
532 this.find_or_create_worktree(&path, false, cx)
533 })?
534 .await?;
535 let buffer = mode
536 .buffer_store
537 .update(cx, |this, cx| {
538 let path = ProjectPath {
539 worktree_id: worktree.read(cx).id(),
540 path: relative_path.into(),
541 };
542 this.open_buffer(path, cx)
543 })?
544 .await;
545 let Ok(buffer) = buffer else {
546 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
547 continue;
548 };
549 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
550
551 let mut breakpoints_for_file =
552 this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
553
554 for bp in bps {
555 let position = snapshot.anchor_after(PointUtf16::new(bp.row, 0));
556 breakpoints_for_file.breakpoints.push((
557 position,
558 Breakpoint {
559 message: bp.message,
560 state: bp.state,
561 condition: bp.condition,
562 hit_condition: bp.hit_condition,
563 },
564 ))
565 }
566 new_breakpoints.insert(path, breakpoints_for_file);
567 }
568 this.update(cx, |this, cx| {
569 log::info!("Finish deserializing breakpoints & initializing breakpoint store");
570 for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
571 (path.to_string_lossy(), bp_in_file.breakpoints.len())
572 }) {
573 let breakpoint_str = if count > 1 {
574 "breakpoints"
575 } else {
576 "breakpoint"
577 };
578 log::info!("Deserialized {count} {breakpoint_str} at path: {path}");
579 }
580
581 this.breakpoints = new_breakpoints;
582
583 cx.notify();
584 })?;
585
586 Ok(())
587 })
588 } else {
589 Task::ready(Ok(()))
590 }
591 }
592
593 #[cfg(any(test, feature = "test-support"))]
594 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
595 self.breakpoints.keys().cloned().collect()
596 }
597}
598
599#[derive(Clone, Copy)]
600pub enum BreakpointUpdatedReason {
601 Toggled,
602 FileSaved,
603}
604
605pub enum BreakpointStoreEvent {
606 ActiveDebugLineChanged,
607 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
608 BreakpointsCleared(Vec<Arc<Path>>),
609}
610
611impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
612
613type BreakpointMessage = Arc<str>;
614
615#[derive(Clone, Debug)]
616pub enum BreakpointEditAction {
617 Toggle,
618 InvertState,
619 EditLogMessage(BreakpointMessage),
620 EditCondition(BreakpointMessage),
621 EditHitCondition(BreakpointMessage),
622}
623
624#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
625pub enum BreakpointState {
626 Enabled,
627 Disabled,
628}
629
630impl BreakpointState {
631 #[inline]
632 pub fn is_enabled(&self) -> bool {
633 matches!(self, BreakpointState::Enabled)
634 }
635
636 #[inline]
637 pub fn is_disabled(&self) -> bool {
638 matches!(self, BreakpointState::Disabled)
639 }
640
641 #[inline]
642 pub fn to_int(&self) -> i32 {
643 match self {
644 BreakpointState::Enabled => 0,
645 BreakpointState::Disabled => 1,
646 }
647 }
648}
649
650#[derive(Clone, Debug, Hash, PartialEq, Eq)]
651pub struct Breakpoint {
652 pub message: Option<BreakpointMessage>,
653 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
654 pub hit_condition: Option<BreakpointMessage>,
655 pub condition: Option<BreakpointMessage>,
656 pub state: BreakpointState,
657}
658
659impl Breakpoint {
660 pub fn new_standard() -> Self {
661 Self {
662 state: BreakpointState::Enabled,
663 hit_condition: None,
664 condition: None,
665 message: None,
666 }
667 }
668
669 pub fn new_condition(hit_condition: &str) -> Self {
670 Self {
671 state: BreakpointState::Enabled,
672 condition: None,
673 hit_condition: Some(hit_condition.into()),
674 message: None,
675 }
676 }
677
678 pub fn new_log(log_message: &str) -> Self {
679 Self {
680 state: BreakpointState::Enabled,
681 hit_condition: None,
682 condition: None,
683 message: Some(log_message.into()),
684 }
685 }
686
687 fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
688 Some(client::proto::Breakpoint {
689 position: Some(serialize_text_anchor(position)),
690 state: match self.state {
691 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
692 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
693 },
694 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
695 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
696 hit_condition: self
697 .hit_condition
698 .as_ref()
699 .map(|s| String::from(s.as_ref())),
700 })
701 }
702
703 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
704 Some(Self {
705 state: match proto::BreakpointState::from_i32(breakpoint.state) {
706 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
707 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
708 },
709 message: breakpoint.message.map(Into::into),
710 condition: breakpoint.condition.map(Into::into),
711 hit_condition: breakpoint.hit_condition.map(Into::into),
712 })
713 }
714
715 #[inline]
716 pub fn is_enabled(&self) -> bool {
717 self.state.is_enabled()
718 }
719
720 #[inline]
721 pub fn is_disabled(&self) -> bool {
722 self.state.is_disabled()
723 }
724}
725
726/// Breakpoint for location within source code.
727#[derive(Clone, Debug, Hash, PartialEq, Eq)]
728pub struct SourceBreakpoint {
729 pub row: u32,
730 pub path: Arc<Path>,
731 pub message: Option<Arc<str>>,
732 pub condition: Option<Arc<str>>,
733 pub hit_condition: Option<Arc<str>>,
734 pub state: BreakpointState,
735}
736
737impl From<SourceBreakpoint> for dap::SourceBreakpoint {
738 fn from(bp: SourceBreakpoint) -> Self {
739 Self {
740 line: bp.row as u64 + 1,
741 column: None,
742 condition: bp
743 .condition
744 .map(|condition| String::from(condition.as_ref())),
745 hit_condition: bp
746 .hit_condition
747 .map(|hit_condition| String::from(hit_condition.as_ref())),
748 log_message: bp.message.map(|message| String::from(message.as_ref())),
749 mode: None,
750 }
751 }
752}