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 max_point = snapshot.max_point_utf16();
595 let point = PointUtf16::new(bp.row, 0);
596 if point > max_point {
597 log::error!("skipping a deserialized breakpoint that's out of range");
598 continue;
599 }
600 let position = snapshot.anchor_after(point);
601 breakpoints_for_file.breakpoints.push((
602 position,
603 Breakpoint {
604 message: bp.message,
605 state: bp.state,
606 condition: bp.condition,
607 hit_condition: bp.hit_condition,
608 },
609 ))
610 }
611 new_breakpoints.insert(path, breakpoints_for_file);
612 }
613 this.update(cx, |this, cx| {
614 log::info!("Finish deserializing breakpoints & initializing breakpoint store");
615 for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
616 (path.to_string_lossy(), bp_in_file.breakpoints.len())
617 }) {
618 let breakpoint_str = if count > 1 {
619 "breakpoints"
620 } else {
621 "breakpoint"
622 };
623 log::info!("Deserialized {count} {breakpoint_str} at path: {path}");
624 }
625
626 this.breakpoints = new_breakpoints;
627
628 cx.notify();
629 })?;
630
631 Ok(())
632 })
633 } else {
634 Task::ready(Ok(()))
635 }
636 }
637
638 #[cfg(any(test, feature = "test-support"))]
639 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
640 self.breakpoints.keys().cloned().collect()
641 }
642}
643
644#[derive(Clone, Copy)]
645pub enum BreakpointUpdatedReason {
646 Toggled,
647 FileSaved,
648}
649
650pub enum BreakpointStoreEvent {
651 ActiveDebugLineChanged,
652 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
653 BreakpointsCleared(Vec<Arc<Path>>),
654}
655
656impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
657
658type BreakpointMessage = Arc<str>;
659
660#[derive(Clone, Debug)]
661pub enum BreakpointEditAction {
662 Toggle,
663 InvertState,
664 EditLogMessage(BreakpointMessage),
665 EditCondition(BreakpointMessage),
666 EditHitCondition(BreakpointMessage),
667}
668
669#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
670pub enum BreakpointState {
671 Enabled,
672 Disabled,
673}
674
675impl BreakpointState {
676 #[inline]
677 pub fn is_enabled(&self) -> bool {
678 matches!(self, BreakpointState::Enabled)
679 }
680
681 #[inline]
682 pub fn is_disabled(&self) -> bool {
683 matches!(self, BreakpointState::Disabled)
684 }
685
686 #[inline]
687 pub fn to_int(&self) -> i32 {
688 match self {
689 BreakpointState::Enabled => 0,
690 BreakpointState::Disabled => 1,
691 }
692 }
693}
694
695#[derive(Clone, Debug, Hash, PartialEq, Eq)]
696pub struct Breakpoint {
697 pub message: Option<BreakpointMessage>,
698 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
699 pub hit_condition: Option<BreakpointMessage>,
700 pub condition: Option<BreakpointMessage>,
701 pub state: BreakpointState,
702}
703
704impl Breakpoint {
705 pub fn new_standard() -> Self {
706 Self {
707 state: BreakpointState::Enabled,
708 hit_condition: None,
709 condition: None,
710 message: None,
711 }
712 }
713
714 pub fn new_condition(hit_condition: &str) -> Self {
715 Self {
716 state: BreakpointState::Enabled,
717 condition: None,
718 hit_condition: Some(hit_condition.into()),
719 message: None,
720 }
721 }
722
723 pub fn new_log(log_message: &str) -> Self {
724 Self {
725 state: BreakpointState::Enabled,
726 hit_condition: None,
727 condition: None,
728 message: Some(log_message.into()),
729 }
730 }
731
732 fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
733 Some(client::proto::Breakpoint {
734 position: Some(serialize_text_anchor(position)),
735 state: match self.state {
736 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
737 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
738 },
739 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
740 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
741 hit_condition: self
742 .hit_condition
743 .as_ref()
744 .map(|s| String::from(s.as_ref())),
745 })
746 }
747
748 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
749 Some(Self {
750 state: match proto::BreakpointState::from_i32(breakpoint.state) {
751 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
752 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
753 },
754 message: breakpoint.message.map(Into::into),
755 condition: breakpoint.condition.map(Into::into),
756 hit_condition: breakpoint.hit_condition.map(Into::into),
757 })
758 }
759
760 #[inline]
761 pub fn is_enabled(&self) -> bool {
762 self.state.is_enabled()
763 }
764
765 #[inline]
766 pub fn is_disabled(&self) -> bool {
767 self.state.is_disabled()
768 }
769}
770
771/// Breakpoint for location within source code.
772#[derive(Clone, Debug, Hash, PartialEq, Eq)]
773pub struct SourceBreakpoint {
774 pub row: u32,
775 pub path: Arc<Path>,
776 pub message: Option<Arc<str>>,
777 pub condition: Option<Arc<str>>,
778 pub hit_condition: Option<Arc<str>>,
779 pub state: BreakpointState,
780}
781
782impl From<SourceBreakpoint> for dap::SourceBreakpoint {
783 fn from(bp: SourceBreakpoint) -> Self {
784 Self {
785 line: bp.row as u64 + 1,
786 column: None,
787 condition: bp
788 .condition
789 .map(|condition| String::from(condition.as_ref())),
790 hit_condition: bp
791 .hit_condition
792 .map(|hit_condition| String::from(hit_condition.as_ref())),
793 log_message: bp.message.map(|message| String::from(message.as_ref())),
794 mode: None,
795 }
796 }
797}