Skip to main content

oo_ide/editor/
highlight_worker.rs

1//! Async syntax highlighting worker.
2//!
3//! Provides background highlighting for large files or slow syntaxes.
4//! The main SyntaxHighlighter handles checkpointing and caching.
5
6use 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    /// Shared buffer lines to avoid cloning the Vec for every request.
26    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        // Scheduler used to track and cancel active highlight jobs.
50        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                                    // Offload CPU-heavy highlighting to the blocking thread pool
70                            // to avoid blocking the Tokio runtime.
71                            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                            // Spawn a blocking task that constructs a local
80                            // SyntaxHighlighter and performs the highlighting. The
81                            // blocking task sends the result back on the oneshot
82                            // channel when complete.
83                            // Bump the generation to cancel any previously running jobs.
84                            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                                // Use the cancellable highlighter variant which checks the
89                                // cancellation token between lines.
90                                let tokens = local.highlight_tokens_cancellable(&lines, version, start_line, count, || token.is_cancelled());
91                                // Only send results if this job was not cancelled.
92                                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/// Lock-free cancellation token based on a generation counter.
135///
136/// Each token captures the generation at creation time. When the scheduler
137/// advances the generation (via [`HighlightScheduler::cancel_and_start`]),
138/// all previously issued tokens become cancelled. No `Mutex`, `Vec`, or
139/// per-job registration is needed.
140#[derive(Clone)]
141pub struct CancellationToken {
142    generation: u64,
143    current: Arc<AtomicU64>,
144}
145
146impl CancellationToken {
147    pub fn is_cancelled(&self) -> bool {
148        // Relaxed is sufficient: the generation counter is the sole piece of
149        // shared state and no other data needs to be ordered relative to it.
150        self.generation != self.current.load(Ordering::Relaxed)
151    }
152}
153
154/// Generation-based highlight job scheduler.
155///
156/// Instead of maintaining a `Mutex<Vec<Arc<AtomicBool>>>` of active jobs,
157/// cancellation is achieved by atomically incrementing a generation counter.
158/// Each [`CancellationToken`] compares its captured generation against the
159/// current value — if they differ, the job is stale and should abort.
160///
161/// This eliminates lock contention, `retain`-based removal, and all per-job
162/// registration overhead.
163pub 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    /// Cancels all previously issued tokens and returns a fresh
181    /// [`CancellationToken`] for the new job.
182    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}