use crate::model::buffer::Buffer;
use crate::primitives::highlighter::{HighlightCategory, HighlightSpan};
use crate::view::theme::Theme;
use std::ops::Range;
use std::sync::Arc;
use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet};
const MAX_PARSE_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone)]
struct CachedSpan {
range: Range<usize>,
category: HighlightCategory,
}
#[derive(Debug, Clone)]
struct TextMateCache {
range: Range<usize>,
spans: Vec<CachedSpan>,
}
pub struct TextMateHighlighter {
syntax: &'static SyntaxReference,
syntax_set: Arc<SyntaxSet>,
cache: Option<TextMateCache>,
last_buffer_len: usize,
}
impl TextMateHighlighter {
pub fn new(syntax: &'static SyntaxReference, syntax_set: Arc<SyntaxSet>) -> Self {
Self {
syntax,
syntax_set,
cache: None,
last_buffer_len: 0,
}
}
pub fn from_syntax_name(_name: &str, _syntax_set: Arc<SyntaxSet>) -> Option<Self> {
None }
pub fn highlight_viewport(
&mut self,
buffer: &Buffer,
viewport_start: usize,
viewport_end: usize,
theme: &Theme,
context_bytes: usize,
) -> Vec<HighlightSpan> {
if let Some(cache) = &self.cache {
if cache.range.start <= viewport_start
&& cache.range.end >= viewport_end
&& self.last_buffer_len == buffer.len()
{
return cache
.spans
.iter()
.filter(|span| {
span.range.start < viewport_end && span.range.end > viewport_start
})
.map(|span| HighlightSpan {
range: span.range.clone(),
color: span.category.color(theme),
})
.collect();
}
}
let parse_start = viewport_start.saturating_sub(context_bytes);
let parse_end = (viewport_end + context_bytes).min(buffer.len());
let parse_range = parse_start..parse_end;
if parse_range.len() > MAX_PARSE_BYTES {
tracing::warn!(
"Parse range too large: {} bytes, skipping TextMate highlighting",
parse_range.len()
);
return Vec::new();
}
let cached_spans = self.parse_region(buffer, parse_start, parse_end);
self.cache = Some(TextMateCache {
range: parse_range,
spans: cached_spans.clone(),
});
self.last_buffer_len = buffer.len();
cached_spans
.into_iter()
.filter(|span| span.range.start < viewport_end && span.range.end > viewport_start)
.map(|span| HighlightSpan {
range: span.range,
color: span.category.color(theme),
})
.collect()
}
fn parse_region(&self, buffer: &Buffer, start_byte: usize, end_byte: usize) -> Vec<CachedSpan> {
let mut spans = Vec::new();
let mut state = ParseState::new(self.syntax);
let content = buffer.slice_bytes(start_byte..end_byte);
let content_str = match std::str::from_utf8(&content) {
Ok(s) => s,
Err(_) => {
tracing::warn!(
"Buffer contains invalid UTF-8 in range {}..{}",
start_byte,
end_byte
);
return spans;
}
};
let mut current_offset = start_byte;
let mut current_scopes = ScopeStack::new();
for line in content_str.lines() {
let line_with_newline = if current_offset + line.len() < end_byte {
format!("{}\n", line)
} else {
line.to_string()
};
let ops = match state.parse_line(&line_with_newline, &self.syntax_set) {
Ok(ops) => ops,
Err(_) => continue, };
let mut char_offset = 0;
for (op_offset, op) in ops {
if op_offset > char_offset {
if let Some(category) = scope_stack_to_category(¤t_scopes) {
let byte_start = current_offset + char_offset;
let byte_end = current_offset + op_offset;
if byte_start < byte_end {
spans.push(CachedSpan {
range: byte_start..byte_end,
category,
});
}
}
}
char_offset = op_offset;
let _ = current_scopes.apply(&op);
}
let line_byte_len = line_with_newline.len();
if char_offset < line_byte_len {
if let Some(category) = scope_stack_to_category(¤t_scopes) {
let byte_start = current_offset + char_offset;
let byte_end = current_offset + line_byte_len;
if byte_start < byte_end {
spans.push(CachedSpan {
range: byte_start..byte_end,
category,
});
}
}
}
current_offset += line_byte_len;
}
merge_adjacent_spans(&mut spans);
spans
}
pub fn invalidate_range(&mut self, edit_range: Range<usize>) {
if let Some(cache) = &self.cache {
if edit_range.start < cache.range.end && edit_range.end > cache.range.start {
self.cache = None;
}
}
}
pub fn invalidate_all(&mut self) {
self.cache = None;
}
pub fn syntax_name(&self) -> &str {
&self.syntax.name
}
}
fn scope_stack_to_category(scopes: &ScopeStack) -> Option<HighlightCategory> {
for scope in scopes.as_slice().iter().rev() {
let scope_str = scope.build_string();
if let Some(category) = scope_to_category(&scope_str) {
return Some(category);
}
}
None
}
pub fn scope_to_category(scope: &str) -> Option<HighlightCategory> {
let scope_lower = scope.to_lowercase();
if scope_lower.starts_with("comment") {
return Some(HighlightCategory::Comment);
}
if scope_lower.starts_with("string") {
return Some(HighlightCategory::String);
}
if scope_lower.starts_with("markup.heading") || scope_lower.starts_with("entity.name.section") {
return Some(HighlightCategory::Keyword); }
if scope_lower.starts_with("markup.bold") {
return Some(HighlightCategory::Constant); }
if scope_lower.starts_with("markup.italic") {
return Some(HighlightCategory::Variable); }
if scope_lower.starts_with("markup.raw") || scope_lower.starts_with("markup.inline.raw") {
return Some(HighlightCategory::String); }
if scope_lower.starts_with("markup.underline.link") {
return Some(HighlightCategory::Function); }
if scope_lower.starts_with("markup.underline") {
return Some(HighlightCategory::Function);
}
if scope_lower.starts_with("markup.quote") {
return Some(HighlightCategory::Comment); }
if scope_lower.starts_with("markup.list") {
return Some(HighlightCategory::Operator); }
if scope_lower.starts_with("markup.strikethrough") {
return Some(HighlightCategory::Comment); }
if scope_lower.starts_with("keyword.control")
|| scope_lower.starts_with("keyword.other")
|| scope_lower.starts_with("keyword.declaration")
|| scope_lower.starts_with("keyword")
{
if !scope_lower.starts_with("keyword.operator") {
return Some(HighlightCategory::Keyword);
}
}
if scope_lower.starts_with("keyword.operator") || scope_lower.starts_with("punctuation") {
return Some(HighlightCategory::Operator);
}
if scope_lower.starts_with("entity.name.function")
|| scope_lower.starts_with("support.function")
|| scope_lower.starts_with("meta.function-call")
|| scope_lower.starts_with("variable.function")
{
return Some(HighlightCategory::Function);
}
if scope_lower.starts_with("entity.name.type")
|| scope_lower.starts_with("entity.name.class")
|| scope_lower.starts_with("entity.name.struct")
|| scope_lower.starts_with("entity.name.enum")
|| scope_lower.starts_with("entity.name.interface")
|| scope_lower.starts_with("entity.name.trait")
|| scope_lower.starts_with("support.type")
|| scope_lower.starts_with("support.class")
|| scope_lower.starts_with("storage.type")
{
return Some(HighlightCategory::Type);
}
if scope_lower.starts_with("storage.modifier") {
return Some(HighlightCategory::Keyword);
}
if scope_lower.starts_with("constant.numeric")
|| scope_lower.starts_with("constant.language.boolean")
{
return Some(HighlightCategory::Number);
}
if scope_lower.starts_with("constant") {
return Some(HighlightCategory::Constant);
}
if scope_lower.starts_with("variable.parameter")
|| scope_lower.starts_with("variable.other")
|| scope_lower.starts_with("variable.language")
{
return Some(HighlightCategory::Variable);
}
if scope_lower.starts_with("entity.name.tag")
|| scope_lower.starts_with("support.other.property")
|| scope_lower.starts_with("meta.object-literal.key")
|| scope_lower.starts_with("variable.other.property")
|| scope_lower.starts_with("variable.other.object.property")
{
return Some(HighlightCategory::Property);
}
if scope_lower.starts_with("entity.other.attribute")
|| scope_lower.starts_with("meta.attribute")
|| scope_lower.starts_with("entity.name.decorator")
{
return Some(HighlightCategory::Attribute);
}
if scope_lower.starts_with("variable") {
return Some(HighlightCategory::Variable);
}
None
}
fn merge_adjacent_spans(spans: &mut Vec<CachedSpan>) {
if spans.len() < 2 {
return;
}
let mut write_idx = 0;
for read_idx in 1..spans.len() {
if spans[write_idx].category == spans[read_idx].category
&& spans[write_idx].range.end == spans[read_idx].range.start
{
spans[write_idx].range.end = spans[read_idx].range.end;
} else {
write_idx += 1;
if write_idx != read_idx {
spans[write_idx] = spans[read_idx].clone();
}
}
}
spans.truncate(write_idx + 1);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scope_to_category_comments() {
assert_eq!(
scope_to_category("comment.line"),
Some(HighlightCategory::Comment)
);
assert_eq!(
scope_to_category("comment.block"),
Some(HighlightCategory::Comment)
);
assert_eq!(
scope_to_category("comment.line.double-slash.rust"),
Some(HighlightCategory::Comment)
);
}
#[test]
fn test_scope_to_category_strings() {
assert_eq!(
scope_to_category("string.quoted.double"),
Some(HighlightCategory::String)
);
assert_eq!(
scope_to_category("string.quoted.single.python"),
Some(HighlightCategory::String)
);
}
#[test]
fn test_scope_to_category_keywords() {
assert_eq!(
scope_to_category("keyword.control.if"),
Some(HighlightCategory::Keyword)
);
assert_eq!(
scope_to_category("keyword.control.loop.rust"),
Some(HighlightCategory::Keyword)
);
}
#[test]
fn test_scope_to_category_operators() {
assert_eq!(
scope_to_category("keyword.operator.arithmetic"),
Some(HighlightCategory::Operator)
);
assert_eq!(
scope_to_category("punctuation.separator"),
Some(HighlightCategory::Operator)
);
}
#[test]
fn test_scope_to_category_functions() {
assert_eq!(
scope_to_category("entity.name.function"),
Some(HighlightCategory::Function)
);
assert_eq!(
scope_to_category("support.function.builtin"),
Some(HighlightCategory::Function)
);
}
#[test]
fn test_scope_to_category_types() {
assert_eq!(
scope_to_category("entity.name.type"),
Some(HighlightCategory::Type)
);
assert_eq!(
scope_to_category("storage.type.rust"),
Some(HighlightCategory::Type)
);
assert_eq!(
scope_to_category("support.class"),
Some(HighlightCategory::Type)
);
}
#[test]
fn test_scope_to_category_numbers() {
assert_eq!(
scope_to_category("constant.numeric.integer"),
Some(HighlightCategory::Number)
);
assert_eq!(
scope_to_category("constant.numeric.float"),
Some(HighlightCategory::Number)
);
}
#[test]
fn test_scope_to_category_markup() {
assert_eq!(
scope_to_category("markup.heading.1.markdown"),
Some(HighlightCategory::Keyword)
);
assert_eq!(
scope_to_category("markup.heading.2"),
Some(HighlightCategory::Keyword)
);
assert_eq!(
scope_to_category("entity.name.section.markdown"),
Some(HighlightCategory::Keyword)
);
assert_eq!(
scope_to_category("markup.bold"),
Some(HighlightCategory::Constant)
);
assert_eq!(
scope_to_category("markup.italic"),
Some(HighlightCategory::Variable)
);
assert_eq!(
scope_to_category("markup.raw.inline"),
Some(HighlightCategory::String)
);
assert_eq!(
scope_to_category("markup.raw.block"),
Some(HighlightCategory::String)
);
assert_eq!(
scope_to_category("markup.underline.link"),
Some(HighlightCategory::Function)
);
assert_eq!(
scope_to_category("markup.quote"),
Some(HighlightCategory::Comment)
);
assert_eq!(
scope_to_category("markup.list.unnumbered"),
Some(HighlightCategory::Operator)
);
assert_eq!(
scope_to_category("markup.strikethrough"),
Some(HighlightCategory::Comment)
);
}
#[test]
fn test_merge_adjacent_spans() {
let mut spans = vec![
CachedSpan {
range: 0..5,
category: HighlightCategory::Keyword,
},
CachedSpan {
range: 5..10,
category: HighlightCategory::Keyword,
},
CachedSpan {
range: 10..15,
category: HighlightCategory::String,
},
];
merge_adjacent_spans(&mut spans);
assert_eq!(spans.len(), 2);
assert_eq!(spans[0].range, 0..10);
assert_eq!(spans[0].category, HighlightCategory::Keyword);
assert_eq!(spans[1].range, 10..15);
assert_eq!(spans[1].category, HighlightCategory::String);
}
}