Skip to main content

lattice_grammar/
cancel.rs

1//! Cancellation tokens for evaluator interruption (DESIGN.md §5.2.5).
2//!
3//! The primitive lives at the protocol layer (so search loops in
4//! [`lattice_core`] can poll it without depending on grammar). This
5//! module re-exports it as [`CancellationToken`] and adds the
6//! grammar-domain `check()` short-circuit that converts a flipped
7//! token into [`crate::CommandError::Cancelled`].
8//!
9//! Every evaluator (motion / operator / text-object / ex-command)
10//! that does meaningful work behind a loop receives a token via its
11//! context struct and polls it on a regular cadence. The DESIGN.md
12//! contract requires evaluators to observe a flip within 100µs --
13//! polling once per inner-loop iteration is more than enough on
14//! modern CPUs (a polled load is ~ns).
15//!
16//! # Sources of cancellation
17//!
18//! - **User Esc.** While an evaluator is running on the document
19//!   actor, the user can interrupt by pressing Esc. The TUI input
20//!   loop flips the token of the in-flight invocation.
21//! - **Deadline timer.** Per `LatencyClass` budget (Reflex < 2ms,
22//!   Display < 10ms). NOT YET WIRED in v1 -- the token type is in
23//!   place so deadline plumbing layers on without touching
24//!   evaluators. v1 uses user-Esc cancellation only.
25//! - **Supersede.** A newer same-event request invalidates the
26//!   in-flight one (e.g. a newer `CompletionRequested` cancels the
27//!   prior LSP request). NOT YET WIRED.
28//!
29//! # Cancellation semantics (§5.2.5)
30//!
31//! On observation of a flipped token an evaluator returns
32//! [`crate::CommandError::Cancelled`]. The dispatcher and the
33//! actor see this as a soft failure: **no `Effect` is committed**,
34//! the document stays at its pre-call version, and any
35//! caller-observable state (cursor, selections, undo stack) is
36//! unchanged. From the user's perspective the keystroke had no
37//! effect -- which is the correct framing since they cancelled it.
38
39use crate::error::{CommandError, GrammarResult};
40
41/// Re-export of the protocol-layer cancellation primitive. Grammar
42/// callers access it under this name; lower crates (search loops in
43/// `lattice-core`) use [`lattice_protocol::CancellationToken`]
44/// directly. The two are the same type.
45pub use lattice_protocol::CancellationToken;
46
47/// Grammar extension: convert a flipped token into a
48/// [`CommandError::Cancelled`] result, so an evaluator's inner loop
49/// threads cancellation through with `?`.
50///
51/// # Examples
52///
53/// ```
54/// use lattice_grammar::{CancellationToken, CheckCancelled, CommandError, GrammarResult};
55///
56/// fn scan(lines: &[&str], cancel: &CancellationToken) -> GrammarResult<usize> {
57///     let mut n = 0;
58///     for line in lines {
59///         cancel.check()?; // poll once per iteration
60///         n += line.len();
61///     }
62///     Ok(n)
63/// }
64///
65/// let token = CancellationToken::new();
66/// assert_eq!(scan(&["ab", "c"], &token).unwrap(), 3);
67///
68/// // The UI flips the token (user Esc); the evaluator bails out and the
69/// // dispatcher commits no effect.
70/// token.cancel();
71/// assert!(matches!(scan(&["ab"], &token), Err(CommandError::Cancelled)));
72/// ```
73pub trait CheckCancelled {
74    /// `Ok(())` while the token is live; [`CommandError::Cancelled`] once
75    /// it has been cancelled. Cheap (one atomic load) -- call it every
76    /// inner-loop iteration.
77    fn check(&self) -> GrammarResult<()>;
78}
79
80impl CheckCancelled for CancellationToken {
81    fn check(&self) -> GrammarResult<()> {
82        if self.is_cancelled() {
83            Err(CommandError::Cancelled)
84        } else {
85            Ok(())
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    #![allow(clippy::unwrap_used, clippy::panic)]
93    use super::*;
94
95    #[test]
96    fn check_on_fresh_token_is_ok() {
97        let t = CancellationToken::new();
98        assert!(t.check().is_ok());
99    }
100
101    #[test]
102    fn check_on_flipped_token_returns_cancelled() {
103        let t = CancellationToken::new();
104        t.cancel();
105        assert!(matches!(t.check(), Err(CommandError::Cancelled)));
106    }
107}