oo_ide/editor/
highlight_worker.rs1use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Arc;
8use std::time::Duration;
9
10use tokio::sync::{mpsc, oneshot};
11
12use crate::editor::buffer::Version;
13use crate::editor::highlight::{SyntaxHighlighter, Token};
14
15pub struct HighlightWorker {
16 tx: mpsc::Sender<HighlightRequest>,
17 handle: tokio::task::JoinHandle<()>,
18}
19
20#[derive(Debug)]
21pub struct HighlightRequest {
22 pub version: Version,
23 pub start_line: usize,
24 pub count: usize,
25 pub lines: Arc<Vec<String>>,
27 pub reply: oneshot::Sender<Vec<Arc<Vec<Token>>>>,
28}
29
30impl HighlightWorker {
31 pub fn new(highlighter: SyntaxHighlighter) -> Self {
32 let (tx, rx) = mpsc::channel::<HighlightRequest>(32);
33
34 let handle = tokio::spawn(async move {
35 Self::run_worker(rx, highlighter).await;
36 });
37 Self { tx, handle }
38 }
39
40 async fn run_worker(
41 mut rx: mpsc::Receiver<HighlightRequest>,
42 highlighter: SyntaxHighlighter,
43 ) {
44 const DEBOUNCE_MS: u64 = 50;
45
46 let mut pending: Option<HighlightRequest> = None;
47 let mut last_request_time = std::time::Instant::now();
48
49 let scheduler = Arc::new(HighlightScheduler::new());
51
52 loop {
53 tokio::select! {
54 biased;
55
56 request = rx.recv() => {
57 match request {
58 None => break,
59 Some(req) => {
60 pending = Some(req);
61 last_request_time = std::time::Instant::now();
62 }
63 }
64 }
65
66 _ = tokio::time::sleep(Duration::from_millis(DEBOUNCE_MS)), if pending.is_some() => {
67 if last_request_time.elapsed() >= Duration::from_millis(DEBOUNCE_MS)
68 && let Some(req) = pending.take() {
69 let theme = highlighter.theme_name.clone();
72 let syntax = highlighter.syntax_name.clone();
73 let lines = req.lines;
74 let version = req.version;
75 let start_line = req.start_line;
76 let count = req.count;
77 let reply = req.reply;
78
79 let token = scheduler.cancel_and_start();
85
86 tokio::task::spawn_blocking(move || {
87 let local = SyntaxHighlighter::new().with_theme(&theme).with_syntax(&syntax);
88 let tokens = local.highlight_tokens_cancellable(&lines, version, start_line, count, || token.is_cancelled());
91 if !token.is_cancelled()
93 && reply.send(tokens).is_err() {
94 log::debug!("Highlight request receiver dropped");
95 }
96 });
97 }
98 }
99 }
100 }
101 }
102
103 pub async fn highlight(
104 &self,
105 version: Version,
106 start_line: usize,
107 count: usize,
108 lines: Arc<Vec<String>>,
109 ) -> Vec<Arc<Vec<Token>>> {
110 let (reply, received) = oneshot::channel();
111
112 let request = HighlightRequest {
113 version,
114 start_line,
115 count,
116 lines,
117 reply,
118 };
119
120 if self.tx.send(request).await.is_err() {
121 log::warn!("Highlight worker channel closed");
122 return Vec::new();
123 }
124
125 received.await.unwrap_or_default()
126 }
127
128 pub async fn shutdown(self) {
129 drop(self.tx);
130 let _ = self.handle.await;
131 }
132}
133
134#[derive(Clone)]
141pub struct CancellationToken {
142 generation: u64,
143 current: Arc<AtomicU64>,
144}
145
146impl CancellationToken {
147 pub fn is_cancelled(&self) -> bool {
148 self.generation != self.current.load(Ordering::Relaxed)
151 }
152}
153
154pub struct HighlightScheduler {
164 generation: Arc<AtomicU64>,
165}
166
167impl Default for HighlightScheduler {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173impl HighlightScheduler {
174 pub fn new() -> Self {
175 Self {
176 generation: Arc::new(AtomicU64::new(0)),
177 }
178 }
179
180 pub fn cancel_and_start(&self) -> CancellationToken {
183 let new_gen = self.generation.fetch_add(1, Ordering::Relaxed) + 1;
184 CancellationToken {
185 generation: new_gen,
186 current: self.generation.clone(),
187 }
188 }
189
190 pub fn current_generation(&self) -> u64 {
191 self.generation.load(Ordering::Relaxed)
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn cancellation_token_starts_valid() {
201 let scheduler = HighlightScheduler::new();
202 let token = scheduler.cancel_and_start();
203 assert!(!token.is_cancelled());
204 }
205
206 #[test]
207 fn cancellation_token_invalidated_by_new_generation() {
208 let scheduler = HighlightScheduler::new();
209 let old_token = scheduler.cancel_and_start();
210 let new_token = scheduler.cancel_and_start();
211
212 assert!(old_token.is_cancelled(), "old token must be cancelled");
213 assert!(!new_token.is_cancelled(), "new token must still be valid");
214 }
215
216 #[test]
217 fn multiple_generations_cancel_all_prior_tokens() {
218 let scheduler = HighlightScheduler::new();
219 let t1 = scheduler.cancel_and_start();
220 let t2 = scheduler.cancel_and_start();
221 let t3 = scheduler.cancel_and_start();
222
223 assert!(t1.is_cancelled());
224 assert!(t2.is_cancelled());
225 assert!(!t3.is_cancelled());
226 }
227
228 #[test]
229 fn scheduler_generation_advances() {
230 let scheduler = HighlightScheduler::new();
231 let gen0 = scheduler.current_generation();
232 let _ = scheduler.cancel_and_start();
233 let gen1 = scheduler.current_generation();
234 assert!(gen1 > gen0);
235 }
236
237 #[test]
238 fn cloned_token_shares_cancellation_state() {
239 let scheduler = HighlightScheduler::new();
240 let token = scheduler.cancel_and_start();
241 let cloned = token.clone();
242
243 assert!(!cloned.is_cancelled());
244 let _ = scheduler.cancel_and_start();
245 assert!(cloned.is_cancelled());
246 assert!(token.is_cancelled());
247 }
248}