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