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::{hash::Hash, ops::Range, path::Path, sync::Arc};
15use text::PointUtf16;
16
17use crate::{buffer_store::BufferStore, worktree_store::WorktreeStore, Project, ProjectPath};
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 }
298
299 if breakpoint_set.breakpoints.is_empty() {
300 self.breakpoints.remove(&abs_path);
301 }
302 if let BreakpointStoreMode::Remote(remote) = &self.mode {
303 if let Some(breakpoint) = breakpoint.1.to_proto(&abs_path, &breakpoint.0) {
304 cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint {
305 project_id: remote._upstream_project_id,
306 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
307 breakpoint: Some(breakpoint),
308 }))
309 .detach();
310 }
311 } else if let Some((client, project_id)) = &self.downstream_client {
312 let breakpoints = self
313 .breakpoints
314 .get(&abs_path)
315 .map(|breakpoint_set| {
316 breakpoint_set
317 .breakpoints
318 .iter()
319 .filter_map(|(anchor, bp)| bp.to_proto(&abs_path, anchor))
320 .collect()
321 })
322 .unwrap_or_default();
323
324 let _ = client.send(proto::BreakpointsForFile {
325 project_id: *project_id,
326 path: abs_path.to_str().map(ToOwned::to_owned).unwrap(),
327 breakpoints,
328 });
329 }
330
331 cx.emit(BreakpointStoreEvent::BreakpointsUpdated(
332 abs_path,
333 BreakpointUpdatedReason::Toggled,
334 ));
335 cx.notify();
336 }
337
338 pub fn on_file_rename(
339 &mut self,
340 old_path: Arc<Path>,
341 new_path: Arc<Path>,
342 cx: &mut Context<Self>,
343 ) {
344 if let Some(breakpoints) = self.breakpoints.remove(&old_path) {
345 self.breakpoints.insert(new_path.clone(), breakpoints);
346
347 cx.notify();
348 }
349 }
350
351 pub fn clear_breakpoints(&mut self, cx: &mut Context<Self>) {
352 let breakpoint_paths = self.breakpoints.keys().cloned().collect();
353 self.breakpoints.clear();
354 cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths));
355 }
356
357 pub fn breakpoints<'a>(
358 &'a self,
359 buffer: &'a Entity<Buffer>,
360 range: Option<Range<text::Anchor>>,
361 buffer_snapshot: &'a BufferSnapshot,
362 cx: &App,
363 ) -> impl Iterator<Item = &'a (text::Anchor, Breakpoint)> + 'a {
364 let abs_path = Self::abs_path_from_buffer(buffer, cx);
365 abs_path
366 .and_then(|path| self.breakpoints.get(&path))
367 .into_iter()
368 .flat_map(move |file_breakpoints| {
369 file_breakpoints.breakpoints.iter().filter({
370 let range = range.clone();
371 move |(position, _)| {
372 if let Some(range) = &range {
373 position.cmp(&range.start, buffer_snapshot).is_ge()
374 && position.cmp(&range.end, buffer_snapshot).is_le()
375 } else {
376 true
377 }
378 }
379 })
380 })
381 }
382
383 pub fn active_position(&self) -> Option<&(SessionId, Arc<Path>, text::Anchor)> {
384 self.active_stack_frame.as_ref()
385 }
386
387 pub fn remove_active_position(
388 &mut self,
389 session_id: Option<SessionId>,
390 cx: &mut Context<Self>,
391 ) {
392 if let Some(session_id) = session_id {
393 self.active_stack_frame
394 .take_if(|(id, _, _)| *id == session_id);
395 } else {
396 self.active_stack_frame.take();
397 }
398
399 cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
400 cx.notify();
401 }
402
403 pub fn set_active_position(
404 &mut self,
405 position: (SessionId, Arc<Path>, text::Anchor),
406 cx: &mut Context<Self>,
407 ) {
408 self.active_stack_frame = Some(position);
409 cx.emit(BreakpointStoreEvent::ActiveDebugLineChanged);
410 cx.notify();
411 }
412
413 pub fn breakpoints_from_path(&self, path: &Arc<Path>, cx: &App) -> Vec<SourceBreakpoint> {
414 self.breakpoints
415 .get(path)
416 .map(|bp| {
417 let snapshot = bp.buffer.read(cx).snapshot();
418 bp.breakpoints
419 .iter()
420 .map(|(position, breakpoint)| {
421 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
422 SourceBreakpoint {
423 row: position,
424 path: path.clone(),
425 state: breakpoint.state,
426 message: breakpoint.message.clone(),
427 }
428 })
429 .collect()
430 })
431 .unwrap_or_default()
432 }
433
434 pub fn all_breakpoints(&self, cx: &App) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
435 self.breakpoints
436 .iter()
437 .map(|(path, bp)| {
438 let snapshot = bp.buffer.read(cx).snapshot();
439 (
440 path.clone(),
441 bp.breakpoints
442 .iter()
443 .map(|(position, breakpoint)| {
444 let position = snapshot.summary_for_anchor::<PointUtf16>(position).row;
445 SourceBreakpoint {
446 row: position,
447 path: path.clone(),
448 message: breakpoint.message.clone(),
449 state: breakpoint.state,
450 }
451 })
452 .collect(),
453 )
454 })
455 .collect()
456 }
457
458 pub fn with_serialized_breakpoints(
459 &self,
460 breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
461 cx: &mut Context<BreakpointStore>,
462 ) -> Task<Result<()>> {
463 if let BreakpointStoreMode::Local(mode) = &self.mode {
464 let mode = mode.clone();
465 cx.spawn(async move |this, cx| {
466 let mut new_breakpoints = BTreeMap::default();
467 for (path, bps) in breakpoints {
468 if bps.is_empty() {
469 continue;
470 }
471 let (worktree, relative_path) = mode
472 .worktree_store
473 .update(cx, |this, cx| {
474 this.find_or_create_worktree(&path, false, cx)
475 })?
476 .await?;
477 let buffer = mode
478 .buffer_store
479 .update(cx, |this, cx| {
480 let path = ProjectPath {
481 worktree_id: worktree.read(cx).id(),
482 path: relative_path.into(),
483 };
484 this.open_buffer(path, cx)
485 })?
486 .await;
487 let Ok(buffer) = buffer else {
488 log::error!("Todo: Serialized breakpoints which do not have buffer (yet)");
489 continue;
490 };
491 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
492
493 let mut breakpoints_for_file =
494 this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
495
496 for bp in bps {
497 let position = snapshot.anchor_after(PointUtf16::new(bp.row, 0));
498 breakpoints_for_file.breakpoints.push((
499 position,
500 Breakpoint {
501 message: bp.message,
502 state: bp.state,
503 },
504 ))
505 }
506 new_breakpoints.insert(path, breakpoints_for_file);
507 }
508 this.update(cx, |this, cx| {
509 this.breakpoints = new_breakpoints;
510 cx.notify();
511 })?;
512
513 Ok(())
514 })
515 } else {
516 Task::ready(Ok(()))
517 }
518 }
519
520 #[cfg(any(test, feature = "test-support"))]
521 pub(crate) fn breakpoint_paths(&self) -> Vec<Arc<Path>> {
522 self.breakpoints.keys().cloned().collect()
523 }
524}
525
526#[derive(Clone, Copy)]
527pub enum BreakpointUpdatedReason {
528 Toggled,
529 FileSaved,
530}
531
532pub enum BreakpointStoreEvent {
533 ActiveDebugLineChanged,
534 BreakpointsUpdated(Arc<Path>, BreakpointUpdatedReason),
535 BreakpointsCleared(Vec<Arc<Path>>),
536}
537
538impl EventEmitter<BreakpointStoreEvent> for BreakpointStore {}
539
540type LogMessage = Arc<str>;
541
542#[derive(Clone, Debug)]
543pub enum BreakpointEditAction {
544 Toggle,
545 InvertState,
546 EditLogMessage(LogMessage),
547}
548
549#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
550pub enum BreakpointState {
551 Enabled,
552 Disabled,
553}
554
555impl BreakpointState {
556 #[inline]
557 pub fn is_enabled(&self) -> bool {
558 matches!(self, BreakpointState::Enabled)
559 }
560
561 #[inline]
562 pub fn is_disabled(&self) -> bool {
563 matches!(self, BreakpointState::Disabled)
564 }
565
566 #[inline]
567 pub fn to_int(&self) -> i32 {
568 match self {
569 BreakpointState::Enabled => 0,
570 BreakpointState::Disabled => 1,
571 }
572 }
573}
574
575#[derive(Clone, Debug, Hash, PartialEq, Eq)]
576pub struct Breakpoint {
577 pub message: Option<Arc<str>>,
578 pub state: BreakpointState,
579}
580
581impl Breakpoint {
582 pub fn new_standard() -> Self {
583 Self {
584 state: BreakpointState::Enabled,
585 message: None,
586 }
587 }
588
589 pub fn new_log(log_message: &str) -> Self {
590 Self {
591 state: BreakpointState::Enabled,
592 message: Some(log_message.into()),
593 }
594 }
595
596 fn to_proto(&self, _path: &Path, position: &text::Anchor) -> Option<client::proto::Breakpoint> {
597 Some(client::proto::Breakpoint {
598 position: Some(serialize_text_anchor(position)),
599 state: match self.state {
600 BreakpointState::Enabled => proto::BreakpointState::Enabled.into(),
601 BreakpointState::Disabled => proto::BreakpointState::Disabled.into(),
602 },
603 message: self.message.as_ref().map(|s| String::from(s.as_ref())),
604 })
605 }
606
607 fn from_proto(breakpoint: client::proto::Breakpoint) -> Option<Self> {
608 Some(Self {
609 state: match proto::BreakpointState::from_i32(breakpoint.state) {
610 Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled,
611 None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled,
612 },
613 message: breakpoint.message.map(|message| message.into()),
614 })
615 }
616
617 #[inline]
618 pub fn is_enabled(&self) -> bool {
619 self.state.is_enabled()
620 }
621
622 #[inline]
623 pub fn is_disabled(&self) -> bool {
624 self.state.is_disabled()
625 }
626}
627
628/// Breakpoint for location within source code.
629#[derive(Clone, Debug, Hash, PartialEq, Eq)]
630pub struct SourceBreakpoint {
631 pub row: u32,
632 pub path: Arc<Path>,
633 pub message: Option<Arc<str>>,
634 pub state: BreakpointState,
635}
636
637impl From<SourceBreakpoint> for dap::SourceBreakpoint {
638 fn from(bp: SourceBreakpoint) -> Self {
639 Self {
640 line: bp.row as u64 + 1,
641 column: None,
642 condition: None,
643 hit_condition: None,
644 log_message: bp.message.map(|message| String::from(message.as_ref())),
645 mode: None,
646 }
647 }
648}