logicaffeine_lsp/
scheduler.rs1use 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
10pub const DEBOUNCE: Duration = Duration::from_millis(150);
14
15pub 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 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 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 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 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 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}