Skip to main content

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}