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 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 this.breakpoints = new_breakpoints;
570 cx.notify();
571 })?;
572
573 Ok(())
574 })
575 } else {
576 Task::ready(Ok(()))
577 }
578 }
579
580 #[cfg(any(test, feature = "test-support"))]
581 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
582 self.breakpoints.keys().cloned().collect()
583 }
584}
585
586#[derive(Clone, Copy)]
587pub enum BreakpointUpdatedReason {
588 Toggled,
589 FileSaved,
590}
591
592pub enum BreakpointStoreEvent {
593 ActiveDebugLineChanged,
594 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
595 BreakpointsCleared(Vec<Arc<Path>>),
596}
597
598impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
599
600type BreakpointMessage = Arc<str>;
601
602#[derive(Clone, Debug)]
603pub enum BreakpointEditAction {
604 Toggle,
605 InvertState,
606 EditLogMessage(BreakpointMessage),
607 EditCondition(BreakpointMessage),
608 EditHitCondition(BreakpointMessage),
609}
610
611#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
612pub enum BreakpointState {
613 Enabled,
614 Disabled,
615}
616
617impl BreakpointState {
618 #[inline]
619 pub fn is_enabled(&self) -> bool {
620 matches!(self, BreakpointState::Enabled)
621 }
622
623 #[inline]
624 pub fn is_disabled(&self) -> bool {
625 matches!(self, BreakpointState::Disabled)
626 }
627
628 #[inline]
629 pub fn to_int(&self) -> i32 {
630 match self {
631 BreakpointState::Enabled => 0,
632 BreakpointState::Disabled => 1,
633 }
634 }
635}
636
637#[derive(Clone, Debug, Hash, PartialEq, Eq)]
638pub struct Breakpoint {
639 pub message: Option<BreakpointMessage>,
640 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
641 pub hit_condition: Option<BreakpointMessage>,
642 pub condition: Option<BreakpointMessage>,
643 pub state: BreakpointState,
644}
645
646impl Breakpoint {
647 pub fn new_standard() -> Self {
648 Self {
649 state: BreakpointState::Enabled,
650 hit_condition: None,
651 condition: None,
652 message: None,
653 }
654 }
655
656 pub fn new_condition(hit_condition: &str) -> Self {
657 Self {
658 state: BreakpointState::Enabled,
659 condition: None,
660 hit_condition: Some(hit_condition.into()),
661 message: None,
662 }
663 }
664
665 pub fn new_log(log_message: &str) -> Self {
666 Self {
667 state: BreakpointState::Enabled,
668 hit_condition: None,
669 condition: None,
670 message: Some(log_message.into()),
671 }
672 }
673
674 fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
675 Some(client::proto::Breakpoint {
676 position: Some(serialize_text_anchor(position)),
677 state: match self.state {
678 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
679 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
680 },
681 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
682 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
683 hit_condition: self
684 .hit_condition
685 .as_ref()
686 .map(|s| String::from(s.as_ref())),
687 })
688 }
689
690 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
691 Some(Self {
692 state: match proto::BreakpointState::from_i32(breakpoint.state) {
693 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
694 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
695 },
696 message: breakpoint.message.map(Into::into),
697 condition: breakpoint.condition.map(Into::into),
698 hit_condition: breakpoint.hit_condition.map(Into::into),
699 })
700 }
701
702 #[inline]
703 pub fn is_enabled(&self) -> bool {
704 self.state.is_enabled()
705 }
706
707 #[inline]
708 pub fn is_disabled(&self) -> bool {
709 self.state.is_disabled()
710 }
711}
712
713/// Breakpoint for location within source code.
714#[derive(Clone, Debug, Hash, PartialEq, Eq)]
715pub struct SourceBreakpoint {
716 pub row: u32,
717 pub path: Arc<Path>,
718 pub message: Option<Arc<str>>,
719 pub condition: Option<Arc<str>>,
720 pub hit_condition: Option<Arc<str>>,
721 pub state: BreakpointState,
722}
723
724impl From<SourceBreakpoint> for dap::SourceBreakpoint {
725 fn from(bp: SourceBreakpoint) -> Self {
726 Self {
727 line: bp.row as u64 + 1,
728 column: None,
729 condition: bp
730 .condition
731 .map(|condition| String::from(condition.as_ref())),
732 hit_condition: bp
733 .hit_condition
734 .map(|hit_condition| String::from(hit_condition.as_ref())),
735 log_message: bp.message.map(|message| String::from(message.as_ref())),
736 mode: None,
737 }
738 }
739}