use std::collections::VecDeque;
use std::{
ops::Range,
sync::{Arc, Mutex},
};
#[cfg(feature = "bundled-themes")]
use crate::theme::BuiltinTextMateTheme;
use crate::{
Catalog, PreparedLanguage, TokenizerOptions,
engine::{state::ScopeStackId as EngineScopeStackId, tokenizer::SharedScopeSink},
tokenizer::{TokenScopes, Tokenizer, TokenizerState},
};
use crate::{
Error, Result, ScopeStackId,
theme::{Style, TextMateTheme},
tokenizer::{HighlightStatus, Scopes, Token, TokenizedDocument},
};
#[derive(Debug, Clone)]
pub struct Theme {
inner: ThemeInner,
}
#[derive(Debug, Clone)]
enum ThemeInner {
#[cfg(feature = "bundled-themes")]
Bundled(&'static TextMateTheme),
Owned(Arc<TextMateTheme>),
}
impl ThemeInner {
fn get(&self) -> &TextMateTheme {
match self {
#[cfg(feature = "bundled-themes")]
Self::Bundled(theme) => theme,
Self::Owned(theme) => theme,
}
}
}
impl Theme {
pub fn from_json(json: &str) -> Result<Self> {
TextMateTheme::from_json(json)
.map(|theme| Self {
inner: ThemeInner::Owned(Arc::new(theme)),
})
.map_err(Error::Theme)
}
#[cfg(feature = "bundled-themes")]
pub fn bundled(name: &str) -> Result<Self> {
let theme = BuiltinTextMateTheme::from_name(name)
.ok_or_else(|| Error::UnknownTheme(name.to_owned()))?;
Ok(Self {
inner: ThemeInner::Bundled(theme.get()),
})
}
pub fn name(&self) -> &str {
self.inner.get().name()
}
#[cfg(feature = "html")]
pub(crate) fn rendering_rules(&self) -> impl Iterator<Item = (&str, Style, bool)> {
self.inner.get().rendering_rules()
}
pub fn from_rules(rules: &[crate::ThemeRule]) -> Result<Self> {
TextMateTheme::from_rules(rules)
.map(|theme| Self {
inner: ThemeInner::Owned(Arc::new(theme)),
})
.map_err(Error::Theme)
}
pub fn default_style(&self) -> Style {
self.inner.get().default_style()
}
pub fn color(&self, name: &str) -> Option<crate::RgbColor> {
self.inner.get().color(name)
}
pub fn resolve(&self, scopes: Scopes<'_>) -> Style {
self.inner.get().resolve_scopes(scopes)
}
pub fn resolve_scope_names(&self, scopes: &[&str]) -> Style {
self.inner.get().resolve_names(scopes)
}
#[cfg(feature = "diagnostics")]
pub fn resolve_with_match(&self, scopes: Scopes<'_>) -> crate::ThemeMatch<'_> {
self.inner.get().inspect_scopes(&scopes)
}
#[cfg(feature = "diagnostics")]
pub fn resolve_style(&self, scopes: Scopes<'_>) -> crate::ResolvedThemeStyle {
let matched = self.resolve_with_match(scopes);
crate::ResolvedThemeStyle {
foreground_matched: matched.foreground_matched,
background_matched: matched.background_matched,
modifiers_matched: matched.modifiers_matched,
style: matched.style,
}
}
#[cfg(all(feature = "bundled-themes", any(feature = "html", feature = "ansi")))]
pub(crate) fn resolve_interned(
&self,
table: &crate::HighlightScopeTable,
stack: ScopeStackId,
) -> Style {
self.inner.get().resolve(table, stack)
}
pub(crate) fn resolve_shared_scope_names(&self, scopes: &[Arc<str>]) -> Style {
self.inner.get().resolve_shared_scope_names(scopes)
}
}
#[derive(Debug, Clone, Copy)]
pub struct HighlighterOptions {
pub prepared_languages: usize,
pub idle_tokenizers_per_language: usize,
pub tokenizer: TokenizerOptions,
}
impl Default for HighlighterOptions {
fn default() -> Self {
Self {
prepared_languages: 16,
idle_tokenizers_per_language: 2,
tokenizer: TokenizerOptions {
line_cache_entries: 1024,
..TokenizerOptions::default()
},
}
}
}
#[derive(Debug, Clone)]
pub struct Highlighter {
inner: Arc<HighlighterInner>,
}
#[derive(Debug)]
struct HighlighterInner {
catalog: Catalog,
options: HighlighterOptions,
cache: Mutex<VecDeque<CachedLanguage>>,
}
#[derive(Debug)]
struct CachedLanguage {
id: String,
prepared: Arc<PreparedLanguage>,
idle: Vec<Tokenizer>,
}
impl Highlighter {
pub fn new(catalog: &Catalog) -> Self {
Self::with_catalog_options(catalog, HighlighterOptions::default())
}
pub fn with_catalog_options(catalog: &Catalog, options: HighlighterOptions) -> Self {
Self {
inner: Arc::new(HighlighterInner {
catalog: catalog.clone(),
options,
cache: Mutex::new(VecDeque::new()),
}),
}
}
#[cfg(feature = "bundled-grammars")]
pub fn bundled() -> Result<Self> {
let catalog = Catalog::bundled();
if catalog.languages().is_empty() {
return Err(Error::Bundle(crate::BundleError::new(
crate::BundleErrorKind::EmptyCatalog,
None,
"the bundled language catalog is empty".to_owned(),
)));
}
Ok(Self::new(&catalog))
}
#[cfg(feature = "bundled-grammars")]
pub fn with_options(options: TokenizerOptions) -> Result<Self> {
Ok(Self::with_catalog_options(
&Catalog::bundled(),
HighlighterOptions {
tokenizer: options,
..HighlighterOptions::default()
},
))
}
pub fn tokenize(&self, language: &str, source: &str) -> Result<TokenizedDocument> {
self.with_tokenizer(language, |tokenizer| tokenizer.tokenize(source))
}
fn checkout(
&self,
language: &str,
take_idle: bool,
) -> Result<(Arc<PreparedLanguage>, Option<Tokenizer>)> {
let canonical = self
.inner
.catalog
.canonical_language(language)
.ok_or_else(|| Error::UnknownLanguage(language.to_owned()))?;
let mut cache = self
.inner
.cache
.lock()
.unwrap_or_else(|error| error.into_inner());
if let Some(index) = cache.iter().position(|entry| entry.id == canonical) {
let mut entry = cache.remove(index).expect("cache index exists");
let tokenizer = if take_idle { entry.idle.pop() } else { None };
let prepared = Arc::clone(&entry.prepared);
cache.push_back(entry);
return Ok((prepared, tokenizer));
}
let prepared = Arc::new(PreparedLanguage::for_highlighter(
&self.inner.catalog,
canonical,
)?);
let tokenizer = take_idle.then(|| prepared.first_tokenizer(self.inner.options.tokenizer));
if self.inner.options.prepared_languages > 0 {
if cache.len() == self.inner.options.prepared_languages {
cache.pop_front();
}
cache.push_back(CachedLanguage {
id: canonical.to_owned(),
prepared: Arc::clone(&prepared),
idle: Vec::new(),
});
}
Ok((prepared, tokenizer))
}
fn with_tokenizer<T>(
&self,
language: &str,
operation: impl FnOnce(&mut Tokenizer) -> T,
) -> Result<T> {
let (prepared, tokenizer) = self.checkout(language, true)?;
let mut tokenizer =
tokenizer.unwrap_or_else(|| prepared.tokenizer(self.inner.options.tokenizer));
let result = operation(&mut tokenizer);
let mut cache = self
.inner
.cache
.lock()
.unwrap_or_else(|error| error.into_inner());
if let Some(entry) = cache
.iter_mut()
.find(|entry| Arc::ptr_eq(&entry.prepared, &prepared))
&& entry.idle.len() < self.inner.options.idle_tokenizers_per_language
{
entry.idle.push(tokenizer);
}
Ok(result)
}
#[cfg(all(feature = "bundled-themes", any(feature = "html", feature = "ansi")))]
fn tokenize_compact(
&self,
language: &str,
source: &str,
) -> Result<(crate::HighlightedText, HighlightStatus)> {
self.with_tokenizer(language, |tokenizer| tokenizer.tokenize_compact(source))
}
#[cfg(feature = "bundled-themes")]
pub fn highlight(
&self,
language: &str,
source: &str,
theme: &str,
) -> Result<HighlightedDocument> {
let theme = Theme::bundled(theme)?;
self.highlight_with_theme(language, source, &theme)
}
#[cfg(all(feature = "bundled-themes", feature = "html"))]
pub fn highlight_html(
&self,
language: &str,
source: &str,
theme: &str,
) -> Result<crate::RenderedOutput> {
self.highlight_html_with_options(language, source, theme, &crate::HtmlOptions::default())
}
#[cfg(all(feature = "bundled-themes", feature = "html"))]
pub fn highlight_html_with_options(
&self,
language: &str,
source: &str,
theme: &str,
options: &crate::HtmlOptions,
) -> Result<crate::RenderedOutput> {
let theme = Theme::bundled(theme)?;
let (tokens, status) = self.tokenize_compact(language, source)?;
crate::render::render_html_compact(source, &tokens, status, &theme, options)
}
#[cfg(all(feature = "bundled-themes", feature = "ansi"))]
pub fn highlight_ansi(
&self,
language: &str,
source: &str,
theme: &str,
) -> Result<crate::RenderedOutput> {
self.highlight_ansi_with_options(language, source, theme, &crate::AnsiOptions::default())
}
#[cfg(all(feature = "bundled-themes", feature = "ansi"))]
pub fn highlight_ansi_with_options(
&self,
language: &str,
source: &str,
theme: &str,
options: &crate::AnsiOptions,
) -> Result<crate::RenderedOutput> {
let theme = Theme::bundled(theme)?;
let (tokens, status) = self.tokenize_compact(language, source)?;
crate::render::render_ansi_compact(source, &tokens, status, &theme, options)
}
pub fn highlight_with_theme(
&self,
language: &str,
source: &str,
theme: &Theme,
) -> Result<HighlightedDocument> {
let tokenized = self.tokenize(language, source)?;
Ok(style_document(tokenized, theme))
}
#[cfg(feature = "bundled-themes")]
pub fn highlight_path(
&self,
path: impl AsRef<std::path::Path>,
source: &str,
theme: &str,
) -> Result<HighlightedDocument> {
let path = path.as_ref().to_string_lossy();
let language = self
.inner
.catalog
.detect(Some(std::path::Path::new(path.as_ref())), source)
.ok_or_else(|| Error::UnknownLanguage(path.into_owned()))?;
self.highlight(language, source, theme)
}
#[cfg(feature = "bundled-themes")]
pub fn session(&self, language: &str, theme: &str) -> Result<HighlightSession> {
let theme = Theme::bundled(theme)?;
self.session_with_theme(language, &theme)
}
pub fn session_with_theme(&self, language: &str, theme: &Theme) -> Result<HighlightSession> {
let (prepared, _) = self.checkout(language, false)?;
let tokenizer = prepared.tokenizer(self.inner.options.tokenizer);
let state = tokenizer.initial_state();
Ok(HighlightSession {
tokenizer,
state,
theme: theme.clone(),
style_cache: IncrementalStyleCache::default(),
})
}
}
pub fn style_document(tokenized: TokenizedDocument, theme: &Theme) -> HighlightedDocument {
let status = tokenized.status();
let lines = tokenized
.lines
.into_iter()
.map(|line| HighlightedLine {
tokens: line
.tokens
.into_iter()
.map(|token| {
let style = theme.resolve(token.scopes());
HighlightedToken { token, style }
})
.collect(),
status: line.status,
})
.collect();
HighlightedDocument {
lines,
status,
default_style: theme.inner.get().default_style(),
}
}
const MAX_INCREMENTAL_STYLE_CACHE_ENTRIES: usize = 8_192;
#[derive(Debug, Default)]
struct IncrementalStyleCache {
styles: Vec<Option<Style>>,
}
impl IncrementalStyleCache {
fn resolve(&mut self, stack: EngineScopeStackId, scopes: &[Arc<str>], theme: &Theme) -> Style {
let index = stack.0 as usize;
if let Some(style) = self.styles.get(index).copied().flatten() {
return style;
}
let style = theme.resolve_shared_scope_names(scopes);
if index < MAX_INCREMENTAL_STYLE_CACHE_ENTRIES {
if self.styles.len() <= index {
self.styles.resize(index + 1, None);
}
self.styles[index] = Some(style);
}
style
}
}
struct IncrementalSpanVecSink<'a> {
line: &'a str,
theme: &'a Theme,
style_cache: &'a mut IncrementalStyleCache,
spans: &'a mut Vec<HighlightedToken>,
}
impl SharedScopeSink for IncrementalSpanVecSink<'_> {
fn reserve(&mut self, span_count: usize) {
if self.spans.capacity() == 0 {
*self.spans = Vec::with_capacity(span_count);
} else if self.spans.capacity() < span_count {
self.spans.reserve(span_count);
}
}
fn push(
&mut self,
range: Range<usize>,
stack: EngineScopeStackId,
scopes: Arc<crate::types::ScopeStorage>,
) {
if let Some(span) = incremental_span(
self.line,
range,
stack,
scopes,
self.theme,
self.style_cache,
) {
self.spans.push(span);
}
}
}
struct IncrementalSpanCallbackSink<'a, F> {
line: &'a str,
theme: &'a Theme,
style_cache: &'a mut IncrementalStyleCache,
callback: F,
}
impl<F: FnMut(HighlightedToken)> SharedScopeSink for IncrementalSpanCallbackSink<'_, F> {
fn reserve(&mut self, _span_count: usize) {}
fn push(
&mut self,
range: Range<usize>,
stack: EngineScopeStackId,
scopes: Arc<crate::types::ScopeStorage>,
) {
if let Some(span) = incremental_span(
self.line,
range,
stack,
scopes,
self.theme,
self.style_cache,
) {
(self.callback)(span);
}
}
}
fn incremental_span(
line: &str,
range: Range<usize>,
stack: EngineScopeStackId,
scopes: Arc<crate::types::ScopeStorage>,
theme: &Theme,
style_cache: &mut IncrementalStyleCache,
) -> Option<HighlightedToken> {
let start = range.start.min(line.len());
let end = range.end.min(line.len());
(start < end && line.is_char_boundary(start) && line.is_char_boundary(end)).then(|| {
HighlightedToken {
style: style_cache.resolve(stack, scopes.shared_names(), theme),
token: Token {
range: start..end,
scopes: TokenScopes {
owner: Some(scopes),
stack: ScopeStackId(stack.0),
},
},
}
})
}
#[derive(Debug)]
pub struct HighlightSession {
tokenizer: Tokenizer,
state: TokenizerState,
theme: Theme,
style_cache: IncrementalStyleCache,
}
impl HighlightSession {
pub fn highlight_line(&mut self, line: &str) -> Result<HighlightedLine> {
let mut spans = Vec::new();
let status = self.highlight_line_into(line, &mut spans)?;
Ok(HighlightedLine {
tokens: spans,
status,
})
}
pub fn highlight_line_into(
&mut self,
line: &str,
spans: &mut Vec<HighlightedToken>,
) -> Result<HighlightStatus> {
self.tokenizer.validate_line(line, &self.state)?;
spans.clear();
let mut sink = IncrementalSpanVecSink {
line,
theme: &self.theme,
style_cache: &mut self.style_cache,
spans,
};
Ok(self
.tokenizer
.tokenize_line_shared_with_validated(line, &mut self.state, &mut sink))
}
pub fn highlight_line_with(
&mut self,
line: &str,
sink: impl FnMut(HighlightedToken),
) -> Result<HighlightStatus> {
let mut sink = IncrementalSpanCallbackSink {
line,
theme: &self.theme,
style_cache: &mut self.style_cache,
callback: sink,
};
self.tokenizer
.tokenize_line_shared_with(line, &mut self.state, &mut sink)
}
pub fn reset(&mut self) {
self.state = self.tokenizer.initial_state();
}
pub fn state(&self) -> &TokenizerState {
&self.state
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HighlightedToken {
token: Token,
style: Style,
}
impl HighlightedToken {
pub fn range(&self) -> Range<usize> {
self.token.range()
}
pub fn scopes(&self) -> Scopes<'_> {
self.token.scopes()
}
pub fn scope_stack(&self) -> Option<ScopeStackId> {
self.token.scope_stack()
}
pub fn style(&self) -> Style {
self.style
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HighlightedLine {
tokens: Vec<HighlightedToken>,
status: HighlightStatus,
}
impl HighlightedLine {
pub fn tokens(&self) -> &[HighlightedToken] {
&self.tokens
}
pub fn status(&self) -> HighlightStatus {
self.status
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HighlightedDocument {
lines: Vec<HighlightedLine>,
status: HighlightStatus,
pub(crate) default_style: Style,
}
impl HighlightedDocument {
pub fn lines(&self) -> &[HighlightedLine] {
&self.lines
}
pub fn status(&self) -> HighlightStatus {
self.status
}
}
#[cfg(all(test, feature = "bundled-grammars"))]
mod incremental_style_cache_tests {
use super::*;
fn test_theme() -> Theme {
Theme::from_json(
r##"{
"name": "Incremental cache test",
"colors": {"editor.foreground": "#010203"},
"tokenColors": [{
"scope": "keyword",
"settings": {"foreground": "#112233", "fontStyle": "bold"}
}]
}"##,
)
.unwrap()
}
#[test]
fn incremental_style_cache_is_exact_and_hard_bounded() {
let theme = test_theme();
let scopes = [Arc::<str>::from("source.test"), Arc::from("keyword.test")];
let expected = theme.resolve_shared_scope_names(&scopes);
let mut cache = IncrementalStyleCache::default();
assert_eq!(
cache.resolve(EngineScopeStackId(3), &scopes, &theme),
expected
);
assert_eq!(cache.styles.len(), 4);
assert_eq!(cache.styles[3], Some(expected));
assert_eq!(
cache.resolve(EngineScopeStackId(3), &scopes, &theme),
expected
);
assert_eq!(
cache.resolve(
EngineScopeStackId(MAX_INCREMENTAL_STYLE_CACHE_ENTRIES as u32 - 1),
&scopes,
&theme,
),
expected
);
assert_eq!(cache.styles.len(), MAX_INCREMENTAL_STYLE_CACHE_ENTRIES);
assert_eq!(
cache.resolve(
EngineScopeStackId(MAX_INCREMENTAL_STYLE_CACHE_ENTRIES as u32),
&scopes,
&theme,
),
expected
);
assert_eq!(cache.styles.len(), MAX_INCREMENTAL_STYLE_CACHE_ENTRIES);
}
#[test]
fn incremental_session_reuses_scope_identity_styles_across_lines() {
let highlighter = Highlighter::bundled().unwrap();
let theme = test_theme();
let mut session = highlighter.session_with_theme("rust", &theme).unwrap();
let first = session.highlight_line("fn cached() {}").unwrap();
let cached_slots = session
.style_cache
.styles
.iter()
.filter(|style| style.is_some())
.count();
let cache_len = session.style_cache.styles.len();
assert!(cached_slots > 0);
session.reset();
let second = session.highlight_line("fn cached() {}").unwrap();
assert_eq!(second, first);
assert_eq!(session.style_cache.styles.len(), cache_len);
assert_eq!(
session
.style_cache
.styles
.iter()
.filter(|style| style.is_some())
.count(),
cached_slots
);
}
}
#[cfg(all(test, feature = "bundled-grammars"))]
mod cache_tests {
use super::*;
#[test]
fn cache_reuses_preparation_for_sessions_and_bounds_idle_state() {
let highlighter = Highlighter::with_catalog_options(
&Catalog::bundled(),
HighlighterOptions {
prepared_languages: 1,
idle_tokenizers_per_language: 1,
..HighlighterOptions::default()
},
);
let (first, _) = highlighter.checkout("rs", false).unwrap();
let (second, _) = highlighter.clone().checkout("rust", false).unwrap();
assert!(Arc::ptr_eq(&first, &second));
let theme = Theme::from_json(r#"{"tokenColors":[]}"#).unwrap();
let mut session = highlighter.session_with_theme("rust", &theme).unwrap();
highlighter.tokenize("rust", "fn main() {}").unwrap();
let (prepared, tokenizer) = highlighter.checkout("rust", true).unwrap();
assert!(tokenizer.is_some());
assert!(Arc::ptr_eq(&first, &prepared));
highlighter.tokenize("json", "{}").unwrap();
let cache = highlighter.inner.cache.lock().unwrap();
assert_eq!(cache.len(), 1);
assert_eq!(cache[0].id, "json");
assert_eq!(cache[0].idle.len(), 1);
drop(cache);
assert!(
session
.highlight_line("fn main() {}")
.unwrap()
.status()
.is_complete()
);
highlighter
.with_tokenizer("json", |_| {
highlighter.tokenize("json", "[]").unwrap();
})
.unwrap();
assert_eq!(highlighter.inner.cache.lock().unwrap()[0].idle.len(), 1);
}
#[test]
fn zero_capacity_disables_retention() {
let highlighter = Highlighter::with_catalog_options(
&Catalog::bundled(),
HighlighterOptions {
prepared_languages: 0,
..HighlighterOptions::default()
},
);
highlighter.tokenize("json", "{}").unwrap();
assert!(highlighter.inner.cache.lock().unwrap().is_empty());
let first = highlighter.checkout("json", false).unwrap().0;
let second = highlighter.checkout("json", false).unwrap().0;
assert!(!Arc::ptr_eq(&first, &second));
}
}