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