Skip to main content

lsp_max/primitives/
debounce.rs

1use std::future::Future;
2use std::sync::Arc;
3use std::time::Duration;
4use tokio::sync::watch;
5use tokio::time;
6
7use lsp_types_max::Uri as Url;
8
9use super::DocumentStore;
10
11/// Handle to a debounced callback.  Clone is O(1); all clones share the same
12/// trigger channel.
13///
14/// The background task exits automatically when the last `DebounceHandle` is
15/// dropped (the watch sender goes away).
16#[derive(Clone, Debug)]
17pub struct DebounceHandle {
18    tx: Arc<watch::Sender<()>>,
19}
20
21impl DebounceHandle {
22    /// Signal that work is pending.  The callback fires once after `delay`
23    /// elapses with no further calls.  Rapid-fire calls reset the timer.
24    pub fn trigger(&self) {
25        let _ = self.tx.send(());
26    }
27}
28
29/// Spawn a debounced async callback and return a shareable trigger handle.
30///
31/// Calling `trigger()` schedules `f` to run after `delay`. If `trigger()` is
32/// called again before the delay expires the timer resets — only one `f()`
33/// invocation happens per quiet window.
34///
35/// ```text
36/// let store = DocumentStore::new();
37/// let sink  = DiagnosticSink::new(client.clone());
38/// let dbc   = debounce(Duration::from_millis(150), move || {
39///     let store = store.clone();
40///     let sink  = sink.clone();
41///     async move { analyze_and_publish(&store, &sink).await }
42/// });
43///
44/// // In did_change:
45/// dbc.trigger();
46/// ```
47pub fn debounce<F, Fut>(delay: Duration, f: F) -> DebounceHandle
48where
49    F: Fn() -> Fut + Send + 'static,
50    Fut: Future<Output = ()> + Send + 'static,
51{
52    let (tx, mut rx) = watch::channel(());
53    let handle = DebounceHandle { tx: Arc::new(tx) };
54
55    tokio::spawn(async move {
56        loop {
57            // Block until the first trigger arrives.
58            if rx.changed().await.is_err() {
59                break;
60            }
61            // Drain rapid-fire triggers within the quiet window.
62            while let Ok(Ok(())) = time::timeout(delay, rx.changed()).await {}
63            f().await;
64        }
65    });
66
67    handle
68}
69
70/// Spawn a debounced callback whose quiet-window scales with the document's
71/// activation density ρ_act.
72///
73/// High-activation documents (many rapid edits) benefit from a longer quiet
74/// window: batching more invalidations into one re-analysis pass reduces
75/// redundant work.  The scaling formula is:
76///
77/// ```text
78/// delay = base_delay × clamp(√(activations / 10), 1.0, 8.0)
79/// ```
80///
81/// At `activations = 0` the multiplier is 1.0 (same as plain `debounce`).
82/// At `activations = 10` the multiplier is 1.0; at 40 it is 2.0; at 640 it
83/// saturates at 8.0 (8 × base_delay maximum).
84///
85/// `store` and `uri` are read at trigger time (not at spawn time), so the
86/// activation count reflects accumulated edits up to that point.
87#[tracing::instrument(
88    name = "debounce_adaptive",
89    skip(store, f),
90    fields(
91        uri = ?uri,
92        base_delay_ms = base_delay.as_millis(),
93        activations = tracing::field::Empty,
94        multiplier = tracing::field::Empty,
95    )
96)]
97pub fn debounce_adaptive<F, Fut>(
98    store: DocumentStore,
99    uri: Url,
100    base_delay: Duration,
101    f: F,
102) -> DebounceHandle
103where
104    F: Fn() -> Fut + Send + 'static,
105    Fut: Future<Output = ()> + Send + 'static,
106{
107    let (tx, mut rx) = watch::channel(());
108    let handle = DebounceHandle { tx: Arc::new(tx) };
109
110    let span = tracing::Span::current();
111    tokio::spawn(async move {
112        loop {
113            if rx.changed().await.is_err() {
114                break;
115            }
116            let acts = store.activation_count(&uri);
117            let multiplier = (acts as f64 / 10.0).sqrt().clamp(1.0, 8.0);
118            span.record("activations", acts);
119            span.record("multiplier", multiplier);
120            let delay = base_delay.mul_f64(multiplier);
121            while let Ok(Ok(())) = time::timeout(delay, rx.changed()).await {}
122            f().await;
123        }
124    });
125
126    handle
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use std::sync::{
133        atomic::{AtomicU64, Ordering},
134        Arc,
135    };
136
137    #[tokio::test]
138    #[ignore]
139    async fn debounce_fires_callback_after_quiet_window() {
140        let counter = Arc::new(AtomicU64::new(0));
141        let c = Arc::clone(&counter);
142        let handle = debounce(Duration::from_millis(20), move || {
143            let c = Arc::clone(&c);
144            async move {
145                c.fetch_add(1, Ordering::SeqCst);
146            }
147        });
148        handle.trigger();
149        tokio::time::sleep(Duration::from_millis(80)).await;
150        assert_eq!(counter.load(Ordering::SeqCst), 1);
151    }
152
153    #[tokio::test]
154    #[ignore]
155    async fn debounce_coalesces_rapid_triggers_into_one_call() {
156        let counter = Arc::new(AtomicU64::new(0));
157        let c = Arc::clone(&counter);
158        let handle = debounce(Duration::from_millis(30), move || {
159            let c = Arc::clone(&c);
160            async move {
161                c.fetch_add(1, Ordering::SeqCst);
162            }
163        });
164        for _ in 0..5 {
165            handle.trigger();
166        }
167        tokio::time::sleep(Duration::from_millis(100)).await;
168        assert_eq!(
169            counter.load(Ordering::SeqCst),
170            1,
171            "five rapid triggers must coalesce into one callback"
172        );
173    }
174
175    #[tokio::test]
176    #[ignore]
177    async fn debounce_handle_clone_shares_trigger_channel() {
178        let counter = Arc::new(AtomicU64::new(0));
179        let c = Arc::clone(&counter);
180        let handle = debounce(Duration::from_millis(20), move || {
181            let c = Arc::clone(&c);
182            async move {
183                c.fetch_add(1, Ordering::SeqCst);
184            }
185        });
186        let handle2 = handle.clone();
187        handle2.trigger();
188        tokio::time::sleep(Duration::from_millis(80)).await;
189        assert_eq!(counter.load(Ordering::SeqCst), 1);
190    }
191}