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