use std::ops::Range;
use crate::grammar::{Grammar, LineState, LineTokenizer, ScopeSpan};
use crate::theme::{StyleCache, StyleScratch, StyleSpan, Theme};
use crate::util::line_starts;
#[derive(Debug, Default)]
pub struct LineBuffer {
pub scopes: Vec<ScopeSpan>,
pub styles: Vec<StyleSpan>,
style_cache: StyleCache,
style_scratch: StyleScratch,
}
#[derive(Debug)]
pub struct LineTokens<'text, 'spans> {
pub line_index: usize,
pub byte_range: Range<usize>,
pub text: &'text str,
pub scopes: &'spans [ScopeSpan],
}
#[derive(Debug)]
pub struct StyledLine<'text, 'spans> {
pub line_index: usize,
pub byte_range: Range<usize>,
pub text: &'text str,
pub scopes: &'spans [ScopeSpan],
pub styles: &'spans [StyleSpan],
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OwnedLineTokens {
pub line_index: usize,
pub byte_range: Range<usize>,
pub scopes: Vec<ScopeSpan>,
}
#[derive(Debug)]
pub struct BlobHighlighter<'text> {
tokenizer: LineTokenizer<'text>,
text: &'text str,
line_starts: Vec<usize>,
states: Vec<Option<LineState>>,
scopes: Vec<Option<Vec<ScopeSpan>>>,
buffer: LineBuffer,
}
impl LineBuffer {
pub fn clear(&mut self) {
self.scopes.clear();
self.styles.clear();
self.style_scratch.clear_line();
}
pub fn tokenize<'a>(
&'a mut self,
grammar: &Grammar,
state: &mut LineState,
line: &str,
) -> &'a [ScopeSpan] {
grammar.tokenize_line_into(state, line, &mut self.scopes);
&self.scopes
}
pub fn style<'a>(
&'a mut self,
grammar: &Grammar,
theme: &Theme,
line: &str,
) -> &'a [StyleSpan] {
self.style_cache.refresh(theme, grammar);
theme.style_line_into(
grammar,
line,
&self.scopes,
&self.style_cache,
&mut self.style_scratch,
&mut self.styles,
);
&self.styles
}
pub fn highlight<'a>(
&'a mut self,
grammar: &Grammar,
theme: &Theme,
state: &mut LineState,
line: &str,
) -> (&'a [ScopeSpan], &'a [StyleSpan]) {
grammar.tokenize_line_into(state, line, &mut self.scopes);
self.style(grammar, theme, line);
(&self.scopes, &self.styles)
}
}
impl<'text> BlobHighlighter<'text> {
pub fn new(grammar: &'text Grammar, text: &'text str) -> Self {
let mut highlighter = Self {
tokenizer: LineTokenizer::new(grammar),
text,
line_starts: Vec::new(),
states: Vec::new(),
scopes: Vec::new(),
buffer: LineBuffer::default(),
};
highlighter.rebuild_caches();
highlighter
}
pub fn reset_text(&mut self, text: &'text str) {
self.text = text;
self.rebuild_caches();
}
fn rebuild_caches(&mut self) {
self.line_starts = line_starts(self.text);
self.states = vec![None; self.line_starts.len() + 1];
self.states[0] = Some(LineState::default());
self.scopes = vec![None; self.line_starts.len()];
}
pub fn line_count(&self) -> usize {
self.line_starts.len()
}
pub fn line(&self, line: usize) -> Option<&'text str> {
let range = self.line_byte_range(line)?;
Some(&self.text[range])
}
pub fn line_byte_range(&self, line: usize) -> Option<Range<usize>> {
(line < self.line_count()).then(|| self.line_range_unchecked(line))
}
pub fn is_state_cached(&self, line: usize) -> bool {
self.states.get(line).is_some_and(Option::is_some)
}
pub fn invalidate_from(&mut self, line: usize) {
let start = line.saturating_add(1).min(self.states.len());
for state in &mut self.states[start..] {
*state = None;
}
let start = line.min(self.scopes.len());
for scopes in &mut self.scopes[start..] {
*scopes = None;
}
}
pub fn ensure_state(&mut self, line: usize) -> Option<&LineState> {
if line > self.line_count() {
return None;
}
if self.states[line].is_none() {
let state = self.compute_state(line)?;
self.states[line] = Some(state);
}
self.states[line].as_ref()
}
pub fn highlight_line_into(&mut self, line: usize, scopes: &mut Vec<ScopeSpan>) -> bool {
if line >= self.line_count() {
scopes.clear();
return false;
}
let Some(mut state) = self.ensure_state(line).cloned() else {
scopes.clear();
return false;
};
let range = self.line_range_unchecked(line);
let text = &self.text[range];
tokenize_or_cache_into(
&mut self.tokenizer,
&mut self.states,
&mut self.scopes,
&mut state,
line,
text,
scopes,
);
true
}
pub fn highlight_range<F>(&mut self, range: Range<usize>, mut f: F)
where
F: FnMut(LineTokens<'_, '_>),
{
let start = range.start.min(self.line_count());
let end = range.end.min(self.line_count());
if start >= end {
return;
}
let Some(mut state) = self.ensure_state(start).cloned() else {
return;
};
let mut scopes = Vec::new();
for line_index in start..end {
let byte_range = self.line_range_unchecked(line_index);
let text = &self.text[byte_range.clone()];
tokenize_or_cache_into(
&mut self.tokenizer,
&mut self.states,
&mut self.scopes,
&mut state,
line_index,
text,
&mut scopes,
);
f(LineTokens {
line_index,
byte_range,
text,
scopes: &scopes,
});
}
}
pub fn highlight_styled_range<F>(&mut self, theme: &Theme, range: Range<usize>, mut f: F)
where
F: FnMut(StyledLine<'_, '_>),
{
let start = range.start.min(self.line_count());
let end = range.end.min(self.line_count());
if start >= end {
return;
}
let Some(mut state) = self.ensure_state(start).cloned() else {
return;
};
let grammar = self.tokenizer.grammar;
for line_index in start..end {
let byte_range = self.line_range_unchecked(line_index);
let text = &self.text[byte_range.clone()];
tokenize_or_cache_into(
&mut self.tokenizer,
&mut self.states,
&mut self.scopes,
&mut state,
line_index,
text,
&mut self.buffer.scopes,
);
self.buffer.style(grammar, theme, text);
f(StyledLine {
line_index,
byte_range,
text,
scopes: &self.buffer.scopes,
styles: &self.buffer.styles,
});
}
}
pub fn highlight_range_into(&mut self, range: Range<usize>, output: &mut Vec<OwnedLineTokens>) {
output.clear();
self.highlight_range(range, |line| {
output.push(OwnedLineTokens {
line_index: line.line_index,
byte_range: line.byte_range,
scopes: line.scopes.to_vec(),
});
});
}
pub fn highlighted_range(&mut self, range: Range<usize>) -> Vec<OwnedLineTokens> {
let mut output = Vec::new();
self.highlight_range_into(range, &mut output);
output
}
fn compute_state(&mut self, line: usize) -> Option<LineState> {
let mut index = line;
while index > 0 && self.states[index].is_none() {
index -= 1;
}
let mut state = self.states[index].clone().unwrap_or_default();
let mut scratch = Vec::new();
while index < line {
let range = self.line_range_unchecked(index);
let text = &self.text[range];
self.tokenizer
.tokenize_line_into(&mut state, text, &mut scratch);
index += 1;
self.states[index] = Some(state.clone());
}
Some(state)
}
fn line_range_unchecked(&self, line: usize) -> Range<usize> {
let start = self.line_starts[line];
let mut end = self
.line_starts
.get(line + 1)
.copied()
.unwrap_or(self.text.len());
let bytes = self.text.as_bytes();
if end > start && bytes[end - 1] == b'\n' {
end -= 1;
}
if end > start && bytes[end - 1] == b'\r' {
end -= 1;
}
start..end
}
}
fn tokenize_or_cache_into(
tokenizer: &mut LineTokenizer<'_>,
states: &mut [Option<LineState>],
scopes_cache: &mut [Option<Vec<ScopeSpan>>],
state: &mut LineState,
line_index: usize,
text: &str,
out: &mut Vec<ScopeSpan>,
) {
if let Some(cached) = scopes_cache[line_index].as_ref()
&& let Some(next_state) = states[line_index + 1].as_ref()
{
out.clone_from(cached);
*state = next_state.clone();
} else {
tokenizer.tokenize_line_into(state, text, out);
states[line_index + 1] = Some(state.clone());
scopes_cache[line_index] = Some(out.clone());
}
}