use std::ops::Range;
use super::highlight::{Highlighter, LineState, TokenKind};
struct Entry {
text: String,
state_in: LineState,
state_out: LineState,
tokens: Vec<(Range<usize>, TokenKind)>,
}
#[derive(Default)]
pub struct HighlightCache {
entries: Vec<Entry>,
}
impl HighlightCache {
pub fn new() -> Self {
Self::default()
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn sync(&mut self, highlighter: &dyn Highlighter, lines: &[String]) -> usize {
let mut recomputed = 0;
let mut state = LineState::default();
for (i, line) in lines.iter().enumerate() {
let hit = self
.entries
.get(i)
.is_some_and(|e| e.state_in == state && e.text == *line);
if hit {
state = self.entries[i].state_out;
continue;
}
let state_in = state;
let tokens = highlighter.line(line, &mut state);
let entry = Entry {
text: line.clone(),
state_in,
state_out: state,
tokens,
};
match self.entries.get_mut(i) {
Some(slot) => *slot = entry,
None => self.entries.push(entry),
}
recomputed += 1;
}
self.entries.truncate(lines.len());
recomputed
}
pub fn tokens(&self, i: usize) -> &[(Range<usize>, TokenKind)] {
self.entries.get(i).map(|e| e.tokens.as_slice()).unwrap_or(&[])
}
}
#[cfg(test)]
mod tests {
use super::super::highlight::Language;
use super::*;
fn doc(lines: &[&str]) -> Vec<String> {
lines.iter().map(|s| s.to_string()).collect()
}
#[test]
fn unchanged_lines_are_not_retokenized() {
let lines = doc(&["let a = 1;", "let b = 2;", "let c = 3;"]);
let mut cache = HighlightCache::new();
assert_eq!(cache.sync(&Language::Rust, &lines), 3);
assert_eq!(cache.sync(&Language::Rust, &lines), 0);
assert!(!cache.tokens(0).is_empty());
}
#[test]
fn editing_one_line_retokenizes_only_that_line() {
let mut lines = doc(&["let a = 1;", "let b = 2;", "let c = 3;"]);
let mut cache = HighlightCache::new();
cache.sync(&Language::Rust, &lines);
lines[1] = "let b = 42;".to_string();
assert_eq!(cache.sync(&Language::Rust, &lines), 1);
let text = &lines[1];
let has_42 = cache
.tokens(1)
.iter()
.any(|(r, k)| &text[r.clone()] == "42" && *k == TokenKind::Number);
assert!(has_42);
}
#[test]
fn opening_a_block_comment_invalidates_the_lines_below() {
let mut lines = doc(&["let a = 1;", "let b = 2;", "let c = 3;"]);
let mut cache = HighlightCache::new();
cache.sync(&Language::Rust, &lines);
lines[0] = "/* let a = 1;".to_string();
assert_eq!(cache.sync(&Language::Rust, &lines), 3);
assert_eq!(cache.tokens(2), &[(0..lines[2].len(), TokenKind::Comment)]);
lines[0] = "let a = 1;".to_string();
assert_eq!(cache.sync(&Language::Rust, &lines), 3);
assert_ne!(cache.tokens(2), &[(0..lines[2].len(), TokenKind::Comment)]);
}
#[test]
fn state_convergence_stops_the_recompute() {
let mut lines = doc(&["/* a", "b */", "let c = 3;", "let d = 4;"]);
let mut cache = HighlightCache::new();
cache.sync(&Language::Rust, &lines);
lines[0] = "/* aa".to_string();
assert_eq!(cache.sync(&Language::Rust, &lines), 1);
}
#[test]
fn line_count_changes_truncate_and_extend() {
let mut lines = doc(&["let a = 1;", "let b = 2;"]);
let mut cache = HighlightCache::new();
cache.sync(&Language::Rust, &lines);
lines.pop();
assert_eq!(cache.sync(&Language::Rust, &lines), 0);
assert!(cache.tokens(1).is_empty()); lines.push("let z = 9;".to_string());
lines.push("let w = 8;".to_string());
assert_eq!(cache.sync(&Language::Rust, &lines), 2);
assert!(!cache.tokens(2).is_empty());
}
#[test]
fn clear_forces_a_full_recompute() {
let lines = doc(&["let a = 1;"]);
let mut cache = HighlightCache::new();
cache.sync(&Language::Rust, &lines);
cache.clear();
assert!(cache.tokens(0).is_empty());
assert_eq!(cache.sync(&Language::Rust, &lines), 1);
}
#[test]
fn out_of_range_lines_are_empty() {
let cache = HighlightCache::new();
assert!(cache.tokens(0).is_empty());
assert!(cache.tokens(99).is_empty());
}
}