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}