Skip to main content

diffs_rs/
highlight.rs

1use std::{
2    collections::{VecDeque, hash_map::RandomState},
3    ops::Range,
4    path::Path,
5    sync::{Arc, Condvar, Mutex, MutexGuard, PoisonError, mpsc},
6    thread,
7};
8
9use ratatui::style::Color;
10use syntect::{
11    highlighting::{HighlightIterator, HighlightState, Highlighter},
12    parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
13};
14
15#[cfg(feature = "delivery-trace")]
16use crate::delivery_trace::{Outcome, ReplayReason, RequestWork};
17#[cfg(feature = "delivery-trace")]
18use std::time::Instant;
19
20use crate::{
21    model::Kind,
22    storage::FileIndex,
23    text::display_text,
24    theme::{self, Appearance},
25    view::LineId,
26};
27
28/// Selects a bundled grammar for a patch path without reading file contents.
29///
30/// Patch-only and checked-source coloring share this choice. Whole file names that a grammar
31/// lists, such as `Makefile`, `Dockerfile`, `Cargo.lock`, and `.bashrc`, take precedence over
32/// extensions, in the same order as Syntect's `find_syntax_for_file`. Unlike Syntect, the
33/// whole-name match is case-sensitive, so a Bazel `BUILD` file does not select the grammar
34/// for `.build` files. Extensions still ignore ASCII case.
35pub(crate) fn syntax_for_path<'s>(
36    path: &str,
37    syntaxes: &'s SyntaxSet,
38) -> Option<&'s SyntaxReference> {
39    let path = Path::new(path);
40    let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
41    let by_name = (!name.is_empty())
42        .then(|| {
43            syntaxes
44                .syntaxes()
45                .iter()
46                .rev()
47                .find(|syntax| syntax.file_extensions.iter().any(|known| known == name))
48        })
49        .flatten();
50    by_name.or_else(|| {
51        let extension = path.extension().and_then(|s| s.to_str())?;
52        syntaxes.find_syntax_by_extension(extension)
53    })
54}
55
56mod checkpoint;
57use checkpoint::Checkpoints;
58mod full_source;
59mod input;
60pub use input::{FileSource, Origin};
61
62mod inline;
63pub use inline::{ChangeSide, changed_spans};
64
65#[cfg(feature = "owner-census")]
66mod cache_activity;
67#[cfg(feature = "owner-census")]
68mod census;
69
70#[derive(Clone, Debug, PartialEq)]
71pub struct Token {
72    pub range: Range<usize>,
73    pub color: Color,
74}
75
76#[derive(Clone, Debug, Default)]
77pub struct LineStyle {
78    pub tokens: Vec<Token>,
79    pub old_tokens: Option<Vec<Token>>,
80    pub changed: Vec<Range<usize>>,
81    pub fallback: bool,
82    pub fallback_reason: Option<Fallback>,
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
86pub enum Fallback {
87    UnsupportedLanguage,
88    LongLine,
89    ParserFailure,
90}
91
92impl Fallback {
93    pub fn status(self) -> &'static str {
94        match self {
95            Self::UnsupportedLanguage => "Plain syntax · unsupported language",
96            Self::LongLine => "Plain syntax · oversized line in source prefix",
97            Self::ParserFailure => "Plain syntax · source parser failed",
98        }
99    }
100}
101
102impl LineStyle {
103    fn cache_weight(&self) -> usize {
104        size_of::<Self>()
105            + self.tokens.capacity() * size_of::<Token>()
106            + self
107                .old_tokens
108                .as_ref()
109                .map_or(0, |tokens| tokens.capacity() * size_of::<Token>())
110            + self.changed.capacity() * size_of::<Range<usize>>()
111    }
112
113    pub fn tokens_for(&self, old: bool) -> &[Token] {
114        if old {
115            self.old_tokens.as_deref().unwrap_or(&self.tokens)
116        } else {
117            &self.tokens
118        }
119    }
120}
121
122fn tokens(fragments: Vec<(syntect::highlighting::Style, &str)>) -> Vec<Token> {
123    let mut byte = 0;
124    fragments
125        .into_iter()
126        .map(|(style, text)| {
127            let start = byte;
128            byte += text.len();
129            Token {
130                range: start..byte,
131                color: Color::Rgb(style.foreground.r, style.foreground.g, style.foreground.b),
132            }
133        })
134        .collect()
135}
136
137struct Side {
138    parse: ParseState,
139    highlight: HighlightState,
140    needs_colors: bool,
141    fallback: Option<Fallback>,
142}
143
144impl Side {
145    fn new(syntax: &SyntaxReference, highlighter: &Highlighter<'_>) -> Self {
146        Self {
147            parse: ParseState::new(syntax),
148            highlight: HighlightState::new(highlighter, ScopeStack::new()),
149            needs_colors: false,
150            fallback: None,
151        }
152    }
153
154    fn advance(
155        &mut self,
156        text: &str,
157        syntaxes: &SyntaxSet,
158        highlighter: &Highlighter<'_>,
159        visible: bool,
160        #[cfg(feature = "delivery-trace")] work: &mut RequestWork,
161        #[cfg(feature = "delivery-trace")] source_side: (LineId, bool),
162    ) -> Option<Vec<Token>> {
163        if self.fallback.is_some() {
164            return None;
165        }
166        #[cfg(feature = "delivery-trace")]
167        let initialization = syntect::parsing::syntax_timing::Window::parser_call();
168        #[cfg(feature = "delivery-trace")]
169        let parse_start = Instant::now();
170        let parsed = self.parse.parse_line(text, syntaxes);
171        #[cfg(feature = "delivery-trace")]
172        let parse_elapsed = parse_start.elapsed();
173        #[cfg(feature = "delivery-trace")]
174        work.parsed(
175            parse_elapsed,
176            visible,
177            source_side.0,
178            source_side.1,
179            initialization.finish(),
180        );
181        let ops = match parsed {
182            Ok(ops) => ops,
183            Err(_) => {
184                self.fallback = Some(Fallback::ParserFailure);
185                return None;
186            }
187        };
188        #[cfg(feature = "delivery-trace")]
189        let scope_start = Instant::now();
190        if visible {
191            if self.needs_colors {
192                let path = std::mem::replace(&mut self.highlight.path, ScopeStack::new());
193                self.highlight = HighlightState::new(highlighter, path);
194                self.needs_colors = false;
195            }
196            let colored = tokens(
197                HighlightIterator::new(&mut self.highlight, &ops, text, highlighter).collect(),
198            );
199            #[cfg(feature = "delivery-trace")]
200            work.summary.visible_colors.record(scope_start);
201            Some(colored)
202        } else {
203            self.needs_colors = true;
204            for (_, op) in ops {
205                if self.highlight.path.apply(&op).is_err() {
206                    self.fallback = Some(Fallback::ParserFailure);
207                    #[cfg(feature = "delivery-trace")]
208                    work.summary.replay_scopes.record(scope_start);
209                    return None;
210                }
211            }
212            #[cfg(feature = "delivery-trace")]
213            work.summary.replay_scopes.record(scope_start);
214            Some(Vec::new())
215        }
216    }
217}
218
219#[derive(Clone, Copy, PartialEq, Eq)]
220enum SyntaxScope {
221    PatchHunk(usize),
222    SourceSide { old: bool },
223}
224
225#[derive(Clone, Copy, PartialEq, Eq)]
226struct SyntaxIdentity {
227    origin: Origin,
228    file: usize,
229    scope: SyntaxScope,
230    epoch: u64,
231    appearance: Appearance,
232}
233
234#[derive(Clone, Copy, PartialEq, Eq)]
235struct SyntaxKey {
236    origin: Origin,
237    file: usize,
238    scope: SyntaxScope,
239}
240
241impl SyntaxIdentity {
242    fn key(self) -> SyntaxKey {
243        SyntaxKey {
244            origin: self.origin,
245            file: self.file,
246            scope: self.scope,
247        }
248    }
249}
250
251struct Continuation {
252    identity: SyntaxIdentity,
253    next: usize,
254    old: Side,
255    new: Side,
256}
257
258pub struct Request<S = usize> {
259    pub epoch: u64,
260    pub appearance: Appearance,
261    pub ids: Vec<S>,
262    pub files: Vec<FileSource>,
263}
264pub struct Reply {
265    pub origin: Origin,
266    pub source_error: Option<Arc<str>>,
267    #[cfg(feature = "delivery-trace")]
268    pub generation: u64,
269    pub epoch: u64,
270    pub id: LineId,
271    pub style: LineStyle,
272}
273
274pub struct Worker {
275    requests: Arc<Mailbox>,
276    replies: mpsc::Receiver<anyhow::Result<Reply>>,
277    thread: Option<thread::JoinHandle<()>>,
278}
279
280#[derive(Default)]
281struct Pending {
282    request: Option<Request<LineId>>,
283    #[cfg(feature = "owner-census")]
284    census: Option<crate::owner_census::Request>,
285    generation: u64,
286    closed: bool,
287}
288
289struct Mailbox {
290    pending: Mutex<Pending>,
291    ready: Condvar,
292    wake: mpsc::SyncSender<()>,
293}
294
295impl Worker {
296    #[cfg(feature = "owner-census")]
297    pub(crate) fn request_census(
298        &self,
299        request: crate::owner_census::Request,
300    ) -> anyhow::Result<()> {
301        let mut pending = self.requests.lock();
302        anyhow::ensure!(
303            !pending.closed && pending.census.is_none(),
304            "Census worker unavailable"
305        );
306        pending.census = Some(request);
307        self.requests.ready.notify_one();
308        Ok(())
309    }
310
311    pub fn try_reply(&self) -> Result<anyhow::Result<Reply>, mpsc::TryRecvError> {
312        // Pair capacity release with the publisher's atomic unlock-and-wait.
313        let _pending = self.requests.lock();
314        let reply = self.replies.try_recv()?;
315        self.requests.ready.notify_one();
316        Ok(reply)
317    }
318
319    pub fn request<S: Into<LineId>>(&self, request: Request<S>) {
320        let mut pending = self.requests.lock();
321        if pending.closed {
322            return;
323        }
324        let epoch = request.epoch;
325        pending.request = Some(Request {
326            epoch,
327            appearance: request.appearance,
328            ids: request.ids.into_iter().map(Into::into).collect(),
329            files: request.files,
330        });
331        pending.generation = pending.generation.wrapping_add(1);
332        crate::delivery_event!("request", pending.generation, epoch);
333        self.requests.ready.notify_one();
334    }
335
336    pub fn cancel(&self) {
337        let mut pending = self.requests.lock();
338        pending.request = None;
339        pending.generation = pending.generation.wrapping_add(1);
340        crate::delivery_event!("cancel", pending.generation, 0);
341        self.requests.ready.notify_one();
342    }
343
344    /// Cancel outstanding work and wait for the syntax worker to release its owners.
345    pub fn close(&mut self) -> anyhow::Result<()> {
346        {
347            let mut pending = self.requests.lock();
348            pending.closed = true;
349            pending.request = None;
350            #[cfg(feature = "owner-census")]
351            {
352                pending.census = None;
353            }
354            self.requests.ready.notify_one();
355        }
356        let result = self.thread.take().map_or(Ok(()), |thread| {
357            thread
358                .join()
359                .map_err(|_| anyhow::anyhow!("The syntax worker panicked"))
360        });
361        while self.replies.try_recv().is_ok() {}
362        result
363    }
364}
365
366impl Drop for Worker {
367    fn drop(&mut self) {
368        let _ = self.close();
369    }
370}
371
372impl Mailbox {
373    /// Locks the shared request state, recovering it after a panic on the other thread.
374    /// The state stays consistent at every unlock, so a worker panic surfaces to the host
375    /// as a disconnected reply channel instead of a second panic on the host thread.
376    fn lock(&self) -> MutexGuard<'_, Pending> {
377        self.pending.lock().unwrap_or_else(PoisonError::into_inner)
378    }
379
380    fn wait<'a>(&self, pending: MutexGuard<'a, Pending>) -> MutexGuard<'a, Pending> {
381        self.ready
382            .wait(pending)
383            .unwrap_or_else(PoisonError::into_inner)
384    }
385
386    fn take(
387        &self,
388        #[cfg(feature = "owner-census")] syntaxes: &syntect::parsing::SyntaxSet,
389        #[cfg(feature = "owner-census")] inspect: impl Fn() -> serde_json::Value,
390    ) -> Option<(u64, Request<LineId>)> {
391        let mut pending = self.lock();
392        loop {
393            if pending.closed {
394                return None;
395            }
396            if let Some(request) = pending.request.take() {
397                return Some((pending.generation, request));
398            }
399            #[cfg(feature = "owner-census")]
400            if let Some(census) = pending.census.take() {
401                drop(pending);
402                census.capture(syntaxes, &inspect);
403                let _ = self.wake.try_send(());
404                pending = self.lock();
405                continue;
406            }
407            pending = self.wait(pending);
408        }
409    }
410
411    fn current(&self, generation: u64) -> bool {
412        let pending = self.lock();
413        !pending.closed && pending.generation == generation
414    }
415
416    fn publish(
417        &self,
418        generation: u64,
419        mut reply: anyhow::Result<Reply>,
420        output: &mpsc::SyncSender<anyhow::Result<Reply>>,
421    ) -> bool {
422        #[cfg(feature = "delivery-trace")]
423        crate::delivery_trace::source_event("ready", generation, reply.as_ref().ok().map(|r| r.id));
424        let mut pending = self.lock();
425        loop {
426            if pending.closed || pending.generation != generation {
427                return false;
428            }
429            #[cfg(feature = "delivery-trace")]
430            crate::delivery_trace::source_event(
431                "offer",
432                generation,
433                reply.as_ref().ok().map(|r| r.id),
434            );
435            match output.try_send(reply) {
436                Ok(()) => {
437                    crate::delivery_event!("published", generation, 0);
438                    drop(pending);
439                    let _ = self.wake.try_send(());
440                    return true;
441                }
442                Err(mpsc::TrySendError::Disconnected(_)) => return false,
443                Err(mpsc::TrySendError::Full(value)) => reply = value,
444            }
445            crate::delivery_event!("full", generation, 0);
446            pending = self.wait(pending);
447        }
448    }
449}
450
451pub fn start(wake: mpsc::SyncSender<()>) -> Worker {
452    let requests = Arc::new(Mailbox {
453        pending: Mutex::default(),
454        ready: Condvar::default(),
455        wake,
456    });
457    let input = requests.clone();
458    let (output, replies) = mpsc::sync_channel(16);
459    let thread = thread::spawn(move || {
460        // Drop the reply sender before notifying, including during unwinding.
461        let _notify_exit = crate::wake::NotifyOnExit(input.wake.clone());
462        let output = output;
463        #[cfg(feature = "delivery-trace")]
464        let syntax_start = Instant::now();
465        let syntaxes = two_face::syntax::extra_newlines();
466        #[cfg(feature = "regex-census")]
467        let _census = crate::regex_census::Capture::new(&syntaxes);
468        #[cfg(feature = "delivery-trace")]
469        crate::delivery_event!("syntax_set_startup", syntax_start.elapsed().as_nanos(), 0);
470        let mut continuation: Option<Continuation> = None;
471        let mut full = full_source::Replay::default();
472        let mut checkpoints = Checkpoints::new();
473        'requests: while let Some((generation, request)) = input.take(
474            #[cfg(feature = "owner-census")]
475            &syntaxes,
476            #[cfg(feature = "owner-census")]
477            || census::worker(&continuation, &full, &checkpoints),
478        ) {
479            #[cfg(feature = "delivery-trace")]
480            let mut work = RequestWork::new(generation, request.epoch, &request.ids);
481            #[cfg(feature = "delivery-trace")]
482            let theme_start = Instant::now();
483            let Ok((_, theme)) = theme::load(request.appearance) else {
484                #[cfg(feature = "delivery-trace")]
485                {
486                    work.summary.theme.record(theme_start);
487                    work.summary.outcome = Outcome::ThemeError;
488                }
489                break;
490            };
491            let highlighter = Highlighter::new(&theme);
492            #[cfg(feature = "delivery-trace")]
493            work.summary.theme.record(theme_start);
494            let mut ids = request.ids.clone();
495            ids.sort_unstable();
496            ids.dedup();
497            #[cfg(feature = "delivery-trace")]
498            {
499                work.summary.requested_unique = Some(ids.len());
500            }
501            for source in &request.files {
502                let mut checkpoint_scope = checkpoints.scope();
503                let checkpoints = checkpoint_scope.cache();
504                let file = source.file();
505                let origin = source.origin();
506                let selected: Vec<_> = ids
507                    .iter()
508                    .copied()
509                    .filter(|id| id.belongs_to(file))
510                    .collect();
511                if !matches!(source, FileSource::Patch(_)) {
512                    continuation = None;
513                    let completed = full.run(full_source::Job {
514                        input: &input,
515                        output: &output,
516                        generation,
517                        request: &request,
518                        source,
519                        wanted: &selected,
520                        syntaxes: &syntaxes,
521                        highlighter: &highlighter,
522                        checkpoints,
523                        #[cfg(feature = "delivery-trace")]
524                        work: &mut work,
525                    });
526                    match completed {
527                        Ok(true) => continue,
528                        Ok(false) => continue 'requests,
529                        Err(error) => {
530                            input.publish(generation, Err(error), &output);
531                            return;
532                        }
533                    }
534                }
535                let patch_ids: Vec<_> = selected.iter().filter_map(|id| id.patch()).collect();
536                let mut wanted = patch_ids.as_slice();
537                full = full_source::Replay::default();
538                let syntax = syntax_for_path(file.path.as_ref(), &syntaxes)
539                    .unwrap_or_else(|| syntaxes.find_syntax_plain_text());
540                while let Some(&first) = wanted.first() {
541                    let hunk_index = file.hunks.partition_point(|h| h.lines.end <= first);
542                    let hunk = &file.hunks[hunk_index];
543                    let count = wanted.partition_point(|id| *id < hunk.lines.end);
544                    let (current, remaining) = wanted.split_at(count);
545                    wanted = remaining;
546                    let last = current[current.len() - 1];
547                    let identity = SyntaxIdentity {
548                        origin,
549                        file: file.id,
550                        scope: SyntaxScope::PatchHunk(hunk_index),
551                        epoch: request.epoch,
552                        appearance: request.appearance,
553                    };
554                    #[cfg(feature = "delivery-trace")]
555                    let mut reason = match continuation.as_ref() {
556                        None => ReplayReason::Missing,
557                        Some(c) if c.identity.file != file.id => ReplayReason::File,
558                        Some(c) if c.identity.scope != identity.scope => ReplayReason::Hunk,
559                        Some(c)
560                            if c.identity.epoch != request.epoch
561                                || c.identity.appearance != request.appearance =>
562                        {
563                            ReplayReason::Epoch
564                        }
565                        Some(c) if c.next > first => ReplayReason::Backward,
566                        Some(_) => ReplayReason::Resume,
567                    };
568                    let active = continuation
569                        .as_ref()
570                        .filter(|c| c.identity == identity && c.next <= first)
571                        .map(|c| c.next);
572                    if let Some(checkpoint) = checkpoints.nearest(identity, first)
573                        && active.is_none_or(|next| checkpoint.next > next)
574                    {
575                        if !input.current(generation) {
576                            #[cfg(feature = "delivery-trace")]
577                            {
578                                work.summary.outcome = Outcome::SupersededOrClosed;
579                            }
580                            continue 'requests;
581                        }
582                        let Some(restored) = checkpoint.restore(identity, &highlighter) else {
583                            continue 'requests;
584                        };
585                        if !input.current(generation) {
586                            #[cfg(feature = "delivery-trace")]
587                            {
588                                work.summary.outcome = Outcome::SupersededOrClosed;
589                            }
590                            continue 'requests;
591                        }
592                        continuation = Some(restored);
593                        #[cfg(feature = "delivery-trace")]
594                        {
595                            reason = ReplayReason::Checkpoint;
596                        }
597                    } else if active.is_none() {
598                        continuation = Some(Continuation {
599                            identity,
600                            next: hunk.lines.start,
601                            old: Side::new(syntax, &highlighter),
602                            new: Side::new(syntax, &highlighter),
603                        });
604                    }
605                    let state = continuation.as_mut().unwrap();
606                    #[cfg(feature = "delivery-trace")]
607                    work.group(file.id, hunk_index, state.next, first..=last, reason);
608                    for id in state.next..=last {
609                        #[cfg(feature = "delivery-trace")]
610                        if id % 32 == 0 {
611                            work.summary.replay_cancellation_checks += 1;
612                        }
613                        if id % 32 == 0 && !input.current(generation) {
614                            #[cfg(feature = "delivery-trace")]
615                            {
616                                work.summary.outcome = Outcome::SupersededOrClosed;
617                            }
618                            continue 'requests;
619                        }
620                        let source = file.info(id);
621                        let visible = current.binary_search(&id).is_ok();
622                        let bytes = match file.bytes(id) {
623                            Ok(bytes) => bytes,
624                            Err(error) => {
625                                #[cfg(feature = "delivery-trace")]
626                                let publish_start = Instant::now();
627                                input.publish(generation, Err(error), &output);
628                                #[cfg(feature = "delivery-trace")]
629                                {
630                                    work.summary.publication.record(publish_start);
631                                    work.summary.outcome = Outcome::SourceError;
632                                }
633                                return;
634                            }
635                        };
636                        let text = display_text(&bytes);
637                        let text = format!("{text}\n");
638                        let too_long = text.len() > 40_004 || text.chars().count() > 10_001;
639                        let mut style = LineStyle {
640                            fallback: too_long,
641                            ..LineStyle::default()
642                        };
643                        if too_long {
644                            if source.old.is_some() {
645                                state.old.fallback = Some(Fallback::LongLine);
646                            }
647                            if source.new.is_some() {
648                                state.new.fallback = Some(Fallback::LongLine);
649                            }
650                        }
651                        if !too_long {
652                            let old_tokens = if source.old.is_some() {
653                                state.old.advance(
654                                    &text,
655                                    &syntaxes,
656                                    &highlighter,
657                                    visible,
658                                    #[cfg(feature = "delivery-trace")]
659                                    &mut work,
660                                    #[cfg(feature = "delivery-trace")]
661                                    (LineId::Patch(id), true),
662                                )
663                            } else {
664                                None
665                            };
666                            let new_tokens = if source.new.is_some() {
667                                state.new.advance(
668                                    &text,
669                                    &syntaxes,
670                                    &highlighter,
671                                    visible,
672                                    #[cfg(feature = "delivery-trace")]
673                                    &mut work,
674                                    #[cfg(feature = "delivery-trace")]
675                                    (LineId::Patch(id), false),
676                                )
677                            } else {
678                                None
679                            };
680                            if visible {
681                                style.fallback |= (source.old.is_some() && old_tokens.is_none())
682                                    || (source.new.is_some() && new_tokens.is_none());
683                                if source.kind == Kind::Deletion {
684                                    style.tokens = old_tokens.unwrap_or_default();
685                                } else {
686                                    style.tokens = new_tokens.unwrap_or_default();
687                                    if source.kind == Kind::Context {
688                                        let old = old_tokens.unwrap_or_default();
689                                        if old != style.tokens {
690                                            style.old_tokens = Some(old);
691                                        }
692                                    }
693                                }
694                            }
695                        }
696                        state.next = id + 1;
697                        #[cfg(feature = "delivery-trace")]
698                        {
699                            work.summary.processed_records += 1;
700                            if visible {
701                                work.summary.requested_records += 1;
702                                work.summary.fallback_requested += usize::from(style.fallback);
703                            } else if id < first {
704                                work.summary.predecessor_records += 1;
705                            } else {
706                                work.summary.other_replay_records += 1;
707                            }
708                        }
709                        if !checkpoints
710                            .capture(state, hunk.lines.start, || input.current(generation))
711                        {
712                            #[cfg(feature = "delivery-trace")]
713                            {
714                                work.summary.outcome = Outcome::SupersededOrClosed;
715                            }
716                            continue 'requests;
717                        }
718                        if !visible {
719                            continue;
720                        }
721                        if !too_long && let Some(partner) = file.partner(id) {
722                            let bytes = match file.bytes(partner) {
723                                Ok(bytes) => bytes,
724                                Err(error) => {
725                                    #[cfg(feature = "delivery-trace")]
726                                    let publish_start = Instant::now();
727                                    input.publish(generation, Err(error), &output);
728                                    #[cfg(feature = "delivery-trace")]
729                                    {
730                                        work.summary.publication.record(publish_start);
731                                        work.summary.outcome = Outcome::SourceError;
732                                    }
733                                    return;
734                                }
735                            };
736                            let other = display_text(&bytes);
737                            if other.len() <= 40_000 && other.chars().count() <= 10_000 {
738                                #[cfg(feature = "delivery-trace")]
739                                let comparison_start = Instant::now();
740                                let text = text.trim_end_matches('\n');
741                                style.changed = if source.kind == Kind::Deletion {
742                                    changed_spans(text, &other, ChangeSide::Old)
743                                } else {
744                                    changed_spans(&other, text, ChangeSide::New)
745                                };
746                                #[cfg(feature = "delivery-trace")]
747                                work.summary.word_comparison.record(comparison_start);
748                            }
749                        }
750                        #[cfg(feature = "delivery-trace")]
751                        let publish_start = Instant::now();
752                        let published = input.publish(
753                            generation,
754                            Ok(Reply {
755                                origin,
756                                source_error: None,
757                                #[cfg(feature = "delivery-trace")]
758                                generation,
759                                epoch: request.epoch,
760                                id: LineId::Patch(id),
761                                style,
762                            }),
763                            &output,
764                        );
765                        #[cfg(feature = "delivery-trace")]
766                        {
767                            work.summary.publication.record(publish_start);
768                            work.summary.style_replies += usize::from(published);
769                        }
770                        if !published {
771                            #[cfg(feature = "delivery-trace")]
772                            {
773                                work.summary.outcome = Outcome::PublicationStopped;
774                            }
775                            continue 'requests;
776                        }
777                    }
778                }
779            }
780            #[cfg(feature = "delivery-trace")]
781            {
782                work.summary.outcome = Outcome::Complete;
783            }
784        }
785    });
786    Worker {
787        requests,
788        replies,
789        thread: Some(thread),
790    }
791}
792
793#[derive(Default)]
794pub struct Cache {
795    lines: hashbrown::HashMap<LineId, LineStyle, RandomState>,
796    order: VecDeque<LineId>,
797    visible: Vec<LineId>,
798    bytes: usize,
799    #[cfg(feature = "owner-census")]
800    activity: cache_activity::Activity,
801}
802
803impl Cache {
804    pub(crate) fn protect_visible(&mut self, mut ids: Vec<LineId>) {
805        ids.sort_unstable();
806        ids.dedup();
807        if self.visible == ids {
808            return;
809        }
810        #[cfg(feature = "owner-census")]
811        let before = cache_activity::Point::observe(self);
812        self.visible = ids;
813        self.trim();
814        #[cfg(feature = "owner-census")]
815        self.activity
816            .release(before, cache_activity::Point::observe(self));
817    }
818
819    fn trim(&mut self) {
820        while self.bytes > 32 * 1024 * 1024 {
821            let Some(index) = self
822                .order
823                .iter()
824                .position(|id| self.visible.binary_search(id).is_err())
825            else {
826                break;
827            };
828            let id = self.order.remove(index).expect("Eviction index is missing");
829            let style = self.lines.remove(&id).expect("Queued style is missing");
830            self.bytes -= style.cache_weight();
831        }
832    }
833
834    pub(crate) fn invalidate(&mut self, file: &FileIndex) {
835        #[cfg(feature = "owner-census")]
836        let before = cache_activity::Point::observe(self);
837        self.order.retain(|id| !id.belongs_to(file));
838        self.lines.retain(|id, style| {
839            if !id.belongs_to(file) {
840                return true;
841            }
842            self.bytes -= style.cache_weight();
843            false
844        });
845        #[cfg(feature = "owner-census")]
846        {
847            let after = cache_activity::Point::observe(self);
848            self.activity.invalidate(before, after);
849        }
850    }
851    pub fn get(&self, id: impl Into<LineId>) -> Option<&LineStyle> {
852        self.lines.get(&id.into())
853    }
854    pub fn insert(&mut self, id: impl Into<LineId>, style: LineStyle) {
855        let id = id.into();
856        if self.lines.contains_key(&id) {
857            #[cfg(feature = "owner-census")]
858            {
859                self.activity.duplicates += 1;
860            }
861            return;
862        }
863        #[cfg(feature = "owner-census")]
864        let before = cache_activity::Point::observe(self);
865        self.bytes += style.cache_weight();
866        self.lines.insert(id, style);
867        self.order.push_back(id);
868        #[cfg(feature = "owner-census")]
869        let inserted = cache_activity::Point::observe(self);
870        self.trim();
871        #[cfg(feature = "owner-census")]
872        {
873            let settled = cache_activity::Point::observe(self);
874            self.activity.admit(before, inserted, settled);
875        }
876    }
877}
878
879#[cfg(test)]
880#[path = "highlight/cache_tests.rs"]
881mod cache_tests;
882
883#[cfg(test)]
884#[path = "highlight/syntax_tests.rs"]
885mod syntax_tests;
886
887#[cfg(test)]
888#[path = "highlight/mailbox_tests.rs"]
889mod mailbox_tests;