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, PartialEq)]
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::ClearDebugLines);
525 cx.notify();
526 }
527
528 pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context<Self>) {
529 if self
530 .active_stack_frame
531 .as_ref()
532 .is_some_and(|active_position| active_position == &position)
533 {
534 return;
535 }
536
537 if self.active_stack_frame.is_some() {
538 cx.emit(BreakpointStoreEvent::ClearDebugLines);
539 }
540
541 self.active_stack_frame = Some(position);
542
543 cx.emit(BreakpointStoreEvent::SetDebugLine);
544 cx.notify();
545 }
546
547 pub fn breakpoint_at_row(
548 &self,
549 path: &Path,
550 row: u32,
551 cx: &App,
552 ) -> Option<(Entity<Buffer>, (text::Anchor, Breakpoint))> {
553 self.breakpoints.get(path).and_then(|breakpoints| {
554 let snapshot = breakpoints.buffer.read(cx).text_snapshot();
555
556 breakpoints
557 .breakpoints
558 .iter()
559 .find(|(anchor, _)| anchor.summary::<Point>(&snapshot).row == row)
560 .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.clone()))
561 })
562 }
563
564 pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
565 self.breakpoints
566 .get(path)
567 .map(|bp| {
568 let snapshot = bp.buffer.read(cx).snapshot();
569 bp.breakpoints
570 .iter()
571 .map(|(position, breakpoint)| {
572 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
573 SourceBreakpoint {
574 row: position,
575 path: path.clone(),
576 state: breakpoint.state,
577 message: breakpoint.message.clone(),
578 condition: breakpoint.condition.clone(),
579 hit_condition: breakpoint.hit_condition.clone(),
580 }
581 })
582 .collect()
583 })
584 .unwrap_or_default()
585 }
586
587 pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
588 self.breakpoints
589 .iter()
590 .map(|(path, bp)| {
591 let snapshot = bp.buffer.read(cx).snapshot();
592 (
593 path.clone(),
594 bp.breakpoints
595 .iter()
596 .map(|(position, breakpoint)| {
597 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
598 SourceBreakpoint {
599 row: position,
600 path: path.clone(),
601 message: breakpoint.message.clone(),
602 state: breakpoint.state,
603 hit_condition: breakpoint.hit_condition.clone(),
604 condition: breakpoint.condition.clone(),
605 }
606 })
607 .collect(),
608 )
609 })
610 .collect()
611 }
612
613 pub fn with_serialized_breakpoints(
614 &self,
615 breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
616 cx: &mut Context<BreakpointStore>,
617 ) -> Task<Result<()>> {
618 if let BreakpointStoreMode::Local(mode) = &self.mode {
619 let mode = mode.clone();
620 cx.spawn(async move |this, cx| {
621 let mut new_breakpoints = BTreeMap::default();
622 for (path, bps) in breakpoints {
623 if bps.is_empty() {
624 continue;
625 }
626 let (worktree, relative_path) = mode
627 .worktree_store
628 .update(cx, |this, cx| {
629 this.find_or_create_worktree(&path, false, cx)
630 })?
631 .await?;
632 let buffer = mode
633 .buffer_store
634 .update(cx, |this, cx| {
635 let path = ProjectPath {
636 worktree_id: worktree.read(cx).id(),
637 path: relative_path.into(),
638 };
639 this.open_buffer(path, cx)
640 })?
641 .await;
642 let Ok(buffer) = buffer else {
643 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
644 continue;
645 };
646 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
647
648 let mut breakpoints_for_file =
649 this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
650
651 for bp in bps {
652 let max_point = snapshot.max_point_utf16();
653 let point = PointUtf16::new(bp.row, 0);
654 if point > max_point {
655 log::error!("skipping a deserialized breakpoint that's out of range");
656 continue;
657 }
658 let position = snapshot.anchor_after(point);
659 breakpoints_for_file.breakpoints.push((
660 position,
661 Breakpoint {
662 message: bp.message,
663 state: bp.state,
664 condition: bp.condition,
665 hit_condition: bp.hit_condition,
666 },
667 ))
668 }
669 new_breakpoints.insert(path, breakpoints_for_file);
670 }
671 this.update(cx, |this, cx| {
672 log::info!("Finish deserializing breakpoints & initializing breakpoint store");
673 for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| {
674 (path.to_string_lossy(), bp_in_file.breakpoints.len())
675 }) {
676 let breakpoint_str = if count > 1 {
677 "breakpoints"
678 } else {
679 "breakpoint"
680 };
681 log::info!("Deserialized {count} {breakpoint_str} at path: {path}");
682 }
683
684 this.breakpoints = new_breakpoints;
685
686 cx.notify();
687 })?;
688
689 Ok(())
690 })
691 } else {
692 Task::ready(Ok(()))
693 }
694 }
695
696 #[cfg(any(test, feature = "test-support"))]
697 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
698 self.breakpoints.keys().cloned().collect()
699 }
700}
701
702#[derive(Clone, Copy)]
703pub enum BreakpointUpdatedReason {
704 Toggled,
705 FileSaved,
706}
707
708pub enum BreakpointStoreEvent {
709 SetDebugLine,
710 ClearDebugLines,
711 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
712 BreakpointsCleared(Vec<Arc<Path>>),
713}
714
715impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
716
717type BreakpointMessage = Arc<str>;
718
719#[derive(Clone, Debug)]
720pub enum BreakpointEditAction {
721 Toggle,
722 InvertState,
723 EditLogMessage(BreakpointMessage),
724 EditCondition(BreakpointMessage),
725 EditHitCondition(BreakpointMessage),
726}
727
728#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
729pub enum BreakpointState {
730 Enabled,
731 Disabled,
732}
733
734impl BreakpointState {
735 #[inline]
736 pub fn is_enabled(&self) -> bool {
737 matches!(self, BreakpointState::Enabled)
738 }
739
740 #[inline]
741 pub fn is_disabled(&self) -> bool {
742 matches!(self, BreakpointState::Disabled)
743 }
744
745 #[inline]
746 pub fn to_int(&self) -> i32 {
747 match self {
748 BreakpointState::Enabled => 0,
749 BreakpointState::Disabled => 1,
750 }
751 }
752}
753
754#[derive(Clone, Debug, Hash, PartialEq, Eq)]
755pub struct Breakpoint {
756 pub message: Option<BreakpointMessage>,
757 /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action)
758 pub hit_condition: Option<BreakpointMessage>,
759 pub condition: Option<BreakpointMessage>,
760 pub state: BreakpointState,
761}
762
763impl Breakpoint {
764 pub fn new_standard() -> Self {
765 Self {
766 state: BreakpointState::Enabled,
767 hit_condition: None,
768 condition: None,
769 message: None,
770 }
771 }
772
773 pub fn new_condition(hit_condition: &str) -> Self {
774 Self {
775 state: BreakpointState::Enabled,
776 condition: None,
777 hit_condition: Some(hit_condition.into()),
778 message: None,
779 }
780 }
781
782 pub fn new_log(log_message: &str) -> Self {
783 Self {
784 state: BreakpointState::Enabled,
785 hit_condition: None,
786 condition: None,
787 message: Some(log_message.into()),
788 }
789 }
790
791 fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
792 Some(client::proto::Breakpoint {
793 position: Some(serialize_text_anchor(position)),
794 state: match self.state {
795 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
796 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
797 },
798 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
799 condition: self.condition.as_ref().map(|s| String::from(s.as_ref())),
800 hit_condition: self
801 .hit_condition
802 .as_ref()
803 .map(|s| String::from(s.as_ref())),
804 })
805 }
806
807 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
808 Some(Self {
809 state: match proto::BreakpointState::from_i32(breakpoint.state) {
810 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
811 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
812 },
813 message: breakpoint.message.map(Into::into),
814 condition: breakpoint.condition.map(Into::into),
815 hit_condition: breakpoint.hit_condition.map(Into::into),
816 })
817 }
818
819 #[inline]
820 pub fn is_enabled(&self) -> bool {
821 self.state.is_enabled()
822 }
823
824 #[inline]
825 pub fn is_disabled(&self) -> bool {
826 self.state.is_disabled()
827 }
828}
829
830/// Breakpoint for location within source code.
831#[derive(Clone, Debug, Hash, PartialEq, Eq)]
832pub struct SourceBreakpoint {
833 pub row: u32,
834 pub path: Arc<Path>,
835 pub message: Option<Arc<str>>,
836 pub condition: Option<Arc<str>>,
837 pub hit_condition: Option<Arc<str>>,
838 pub state: BreakpointState,
839}
840
841impl From<SourceBreakpoint> for dap::SourceBreakpoint {
842 fn from(bp: SourceBreakpoint) -> Self {
843 Self {
844 line: bp.row as u64 + 1,
845 column: None,
846 condition: bp
847 .condition
848 .map(|condition| String::from(condition.as_ref())),
849 hit_condition: bp
850 .hit_condition
851 .map(|hit_condition| String::from(hit_condition.as_ref())),
852 log_message: bp.message.map(|message| String::from(message.as_ref())),
853 mode: None,
854 }
855 }
856}