use super::{BlockState, Highlight, Highlighter};
use crate::editor::document::Document;
#[derive(Default)]
pub struct HighlightCache {
highlighter: Option<&'static Highlighter>,
states: Vec<BlockState>,
}
impl std::fmt::Debug for HighlightCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HighlightCache")
.field("language", &self.language_name())
.field("cached_lines", &self.states.len())
.finish()
}
}
impl HighlightCache {
#[must_use]
pub fn new(highlighter: Option<&'static Highlighter>) -> Self {
Self {
highlighter,
states: Vec::new(),
}
}
pub fn set_language(&mut self, highlighter: Option<&'static Highlighter>) {
self.highlighter = highlighter;
self.states.clear();
}
#[must_use]
pub fn language_name(&self) -> &'static str {
self.highlighter.map_or("plain", |h| h.language.name)
}
pub fn invalidate_from(&mut self, line: usize) {
self.states.truncate(line + 1);
}
pub fn ensure(&mut self, document: &Document, upto: usize) {
let Some(highlighter) = self.highlighter else {
return;
};
self.states.truncate(document.len_lines());
if self.states.is_empty() {
self.states.push(BlockState::Normal);
}
let target = upto.min(document.last_line());
while self.states.len() <= target {
let line = self.states.len() - 1;
let state = self.states[line];
let (_, next) = highlighter.highlight_line(&document.line_string(line), state);
self.states.push(next);
}
}
#[must_use]
pub fn highlight(&self, document: &Document, line: usize) -> Vec<Highlight> {
let Some(highlighter) = self.highlighter else {
return Vec::new();
};
let Some(state) = self.states.get(line).copied() else {
return Vec::new();
};
highlighter
.highlight_line(&document.line_string(line), state)
.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::syntax::by_name;
fn document(text: &str) -> Document {
Document::from_text(text, None)
}
#[test]
fn plain_text_produces_no_spans() {
let mut cache = HighlightCache::new(None);
let doc = document("anything");
cache.ensure(&doc, 0);
assert!(cache.highlight(&doc, 0).is_empty());
assert_eq!(cache.language_name(), "plain");
}
#[test]
fn block_comment_state_carries_down_the_file() {
let mut cache = HighlightCache::new(by_name("c"));
let doc = document("/* open\nstill\nstill\n*/ int x;");
cache.ensure(&doc, 3);
assert_eq!(cache.highlight(&doc, 1).len(), 1);
assert!(cache.highlight(&doc, 3).len() > 1);
}
#[test]
fn ensure_only_extends_as_far_as_asked() {
let mut cache = HighlightCache::new(by_name("rust"));
let doc = document(&"let x = 1;\n".repeat(1000));
cache.ensure(&doc, 10);
assert_eq!(cache.states.len(), 11);
}
#[test]
fn an_edit_invalidates_only_the_lines_below_it() {
let mut cache = HighlightCache::new(by_name("rust"));
let doc = document(&"let x = 1;\n".repeat(100));
cache.ensure(&doc, 50);
cache.invalidate_from(20);
assert_eq!(cache.states.len(), 21);
}
#[test]
fn a_shrunk_document_drops_stale_entries() {
let mut cache = HighlightCache::new(by_name("rust"));
let long = document(&"let x = 1;\n".repeat(50));
cache.ensure(&long, 40);
let short = document("let x = 1;\n");
cache.ensure(&short, 1);
assert!(cache.states.len() <= short.len_lines());
}
}