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