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