escriba_runtime/breakpoint.rs
1//! Breakpoints — where the operator asked execution to stop.
2//!
3//! ## Why this is not a [`Finding`](escriba_shirube::Finding)
4//!
5//! Everything else escriba paints in the gutter is a finding, and reusing
6//! that plane was the obvious move. It is also wrong, twice over, and both
7//! failures are silent:
8//!
9//! - **Every `escriba_shirube` list is ANCHORED**, and the gutter's only
10//! reader flat-maps `ResultList::fresh(world)`. That is exactly right for a
11//! diagnostic — a marker computed against text the operator has since
12//! edited is confidently wrong — and exactly wrong for a breakpoint, which
13//! would vanish on the next keystroke.
14//! - **`ListRegistry::publish` replaces a list wholesale and FOCUSES it**, so
15//! `]d` would start walking breakpoints as though they were problems.
16//!
17//! A finding is something a PRODUCER found and is only as good as the world
18//! it was computed in. A breakpoint is something the OPERATOR put there, and
19//! it is as good as their intention, which no edit invalidates.
20//!
21//! ## What this does NOT do yet, stated plainly
22//!
23//! A breakpoint is keyed by `(buffer, line number)` and **does not shift when
24//! text above it is edited**. Insert a line at the top of the file and the
25//! breakpoint stays on the line NUMBER it was set on, not on the line of code
26//! it was set against. Nothing in escriba shifts a mark under an edit today —
27//! findings dodge the problem by dying, which is the option a breakpoint does
28//! not have — so this is the honest floor rather than a bug that slipped
29//! through: the operator's breakpoint survives their typing, which is the
30//! property that matters most, and it can drift.
31//!
32//! **Shifting under an edit is the next piece**, and it is a shared primitive
33//! rather than a patch here: the same machinery would fix findings, marks
34//! (`m[a-z]`), and the jumplist. Do not paper over it with an ad-hoc
35//! adjustment in this file.
36
37use std::collections::BTreeSet;
38
39use escriba_core::BufferId;
40
41/// Every line the operator has marked for the debugger to stop on.
42///
43/// Keyed by `(buffer, line)` — see the module docs for what that key does and
44/// does not survive.
45#[derive(Debug, Clone, Default, PartialEq, Eq)]
46pub struct Breakpoints {
47 set: BTreeSet<(BufferId, u32)>,
48}
49
50impl Breakpoints {
51 /// Set a breakpoint on `line` of `buffer` if there is none, clear it if
52 /// there is. Returns whether one is set AFTERWARDS.
53 ///
54 /// One verb rather than `set` + `clear`, because the operator's key is
55 /// one verb: a pair would let a caller ask "is it set?" and then act on
56 /// the answer, which is the shape a double-toggle race lives in.
57 pub fn toggle(&mut self, buffer: BufferId, line: u32) -> bool {
58 if self.set.remove(&(buffer, line)) {
59 false
60 } else {
61 self.set.insert((buffer, line));
62 true
63 }
64 }
65
66 /// Is there a breakpoint on `line` of `buffer`?
67 #[must_use]
68 pub fn is_set(&self, buffer: BufferId, line: u32) -> bool {
69 self.set.contains(&(buffer, line))
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 const A: BufferId = BufferId(1);
78 const B: BufferId = BufferId(2);
79
80 #[test]
81 fn toggling_is_its_own_inverse() {
82 let mut bp = Breakpoints::default();
83 assert!(!bp.is_set(A, 3));
84 assert!(bp.toggle(A, 3), "the first toggle SETS");
85 assert!(bp.is_set(A, 3));
86 assert!(!bp.toggle(A, 3), "the second toggle CLEARS");
87 assert!(!bp.is_set(A, 3));
88 }
89
90 #[test]
91 fn a_breakpoint_belongs_to_one_buffer() {
92 // The key is the PAIR. Keying on the line alone would put a
93 // breakpoint set in one file onto the same row of every other one —
94 // which looks correct in any single-buffer test.
95 let mut bp = Breakpoints::default();
96 bp.toggle(A, 7);
97 assert!(bp.is_set(A, 7));
98 assert!(!bp.is_set(B, 7), "buffer B never had one");
99 assert!(!bp.is_set(A, 8), "and neither did line 8");
100 }
101}