Skip to main content

logicaffeine_lsp/
scheduler.rs

1use std::sync::atomic::{AtomicU64, Ordering};
2use std::sync::Arc;
3use std::time::Duration;
4
5use dashmap::DashMap;
6use tower_lsp::lsp_types::{TextDocumentContentChangeEvent, Url};
7
8use crate::document::apply_content_change;
9
10/// How long after the last keystroke analysis runs. A typing burst coalesces
11/// into one pass over the final text; a single edit still feels instant
12/// because publish follows the window immediately.
13pub const DEBOUNCE: Duration = Duration::from_millis(150);
14
15/// The live (pre-analysis) text of every open document, with a generation
16/// counter per document.
17///
18/// The generation guard is the cancellation model: every edit bumps the
19/// generation, and an analysis pass only installs+publishes its result if the
20/// generation it captured is still current when it finishes. Stale results
21/// are dropped on the floor — no locks held, nothing blocks.
22pub struct Scheduler {
23    entries: DashMap<Url, DocEntry>,
24}
25
26struct DocEntry {
27    generation: Arc<AtomicU64>,
28    text: String,
29    version: i32,
30}
31
32impl Scheduler {
33    pub fn new() -> Self {
34        Scheduler {
35            entries: DashMap::new(),
36        }
37    }
38
39    /// Register a newly opened document. Returns its starting generation.
40    pub fn open(&self, uri: Url, text: String, version: i32) -> u64 {
41        let entry = DocEntry {
42            generation: Arc::new(AtomicU64::new(0)),
43            text,
44            version,
45        };
46        self.entries.insert(uri, entry);
47        0
48    }
49
50    /// Apply LSP content changes in order and bump the generation.
51    /// Returns the new generation, or `None` for an unopened document.
52    pub fn apply_changes(
53        &self,
54        uri: &Url,
55        changes: Vec<TextDocumentContentChangeEvent>,
56        version: i32,
57    ) -> Option<u64> {
58        let mut entry = self.entries.get_mut(uri)?;
59        for change in changes {
60            apply_content_change(&mut entry.text, change.range, &change.text);
61        }
62        entry.version = version;
63        Some(entry.generation.fetch_add(1, Ordering::SeqCst) + 1)
64    }
65
66    /// Snapshot the current text/version if `generation` is still current.
67    pub fn current_if(&self, uri: &Url, generation: u64) -> Option<(String, i32)> {
68        let entry = self.entries.get(uri)?;
69        if entry.generation.load(Ordering::SeqCst) != generation {
70            return None;
71        }
72        Some((entry.text.clone(), entry.version))
73    }
74
75    /// The live text and version of an open document, regardless of
76    /// generation — what a save should check.
77    pub fn current_text(&self, uri: &Url) -> Option<(String, i32)> {
78        let entry = self.entries.get(uri)?;
79        Some((entry.text.clone(), entry.version))
80    }
81
82    pub fn is_current(&self, uri: &Url, generation: u64) -> bool {
83        self.entries
84            .get(uri)
85            .map(|entry| entry.generation.load(Ordering::SeqCst) == generation)
86            .unwrap_or(false)
87    }
88
89    /// Every open document, for whole-server sweeps (config changes).
90    pub fn open_uris(&self) -> Vec<Url> {
91        self.entries.iter().map(|e| e.key().clone()).collect()
92    }
93
94    pub fn close(&self, uri: &Url) {
95        self.entries.remove(uri);
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    fn uri() -> Url {
104        Url::parse("file:///t.lg").unwrap()
105    }
106
107    fn full_change(text: &str) -> TextDocumentContentChangeEvent {
108        TextDocumentContentChangeEvent {
109            range: None,
110            range_length: None,
111            text: text.to_string(),
112        }
113    }
114
115    #[test]
116    fn open_starts_at_generation_zero() {
117        let scheduler = Scheduler::new();
118        assert_eq!(scheduler.open(uri(), "a".into(), 1), 0);
119        assert!(scheduler.is_current(&uri(), 0));
120    }
121
122    #[test]
123    fn every_change_bumps_the_generation() {
124        let scheduler = Scheduler::new();
125        scheduler.open(uri(), "a".into(), 1);
126        assert_eq!(scheduler.apply_changes(&uri(), vec![full_change("b")], 2), Some(1));
127        assert_eq!(scheduler.apply_changes(&uri(), vec![full_change("c")], 3), Some(2));
128        assert!(!scheduler.is_current(&uri(), 1), "older generations are stale");
129        assert!(scheduler.is_current(&uri(), 2));
130    }
131
132    #[test]
133    fn current_if_returns_text_only_for_the_live_generation() {
134        let scheduler = Scheduler::new();
135        scheduler.open(uri(), "a".into(), 1);
136        let generation = scheduler
137            .apply_changes(&uri(), vec![full_change("newest")], 2)
138            .unwrap();
139        assert_eq!(scheduler.current_if(&uri(), generation), Some(("newest".into(), 2)));
140        assert_eq!(scheduler.current_if(&uri(), generation - 1), None);
141    }
142
143    #[test]
144    fn changes_to_unopened_documents_are_ignored() {
145        let scheduler = Scheduler::new();
146        assert_eq!(scheduler.apply_changes(&uri(), vec![full_change("x")], 1), None);
147    }
148
149    #[test]
150    fn multiple_changes_apply_in_order() {
151        let scheduler = Scheduler::new();
152        scheduler.open(uri(), "Let x be 5.".into(), 1);
153        let generation = scheduler
154            .apply_changes(
155                &uri(),
156                vec![full_change("Let y be 6."), full_change("Let z be 7.")],
157                2,
158            )
159            .unwrap();
160        assert_eq!(
161            scheduler.current_if(&uri(), generation),
162            Some(("Let z be 7.".into(), 2))
163        );
164    }
165
166    #[test]
167    fn close_forgets_the_document() {
168        let scheduler = Scheduler::new();
169        scheduler.open(uri(), "a".into(), 1);
170        scheduler.close(&uri());
171        assert!(!scheduler.is_current(&uri(), 0));
172    }
173}