use streaming_iterator::StreamingIterator;
mod guides;
pub mod languages;
pub use guides::{GuideFrame, IndentGuides};
mod injections;
mod spans;
pub use spans::Emphasis;
use spans::{CaptureStyle, LayeredSpan};
use ropey::Rope;
use strop_core::id::BufferRevision;
use tree_sitter::{Parser, Query, QueryCursor, TextProvider};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum Class {
Keyword,
Function,
Type,
String,
Comment,
Number,
Operator,
Punctuation,
Constant,
Variable,
Attribute,
Heading,
Link,
Code,
Quote,
List,
Tag,
}
impl Class {
fn from_capture(name: &str) -> Self {
if name.starts_with("constant.numeric.") {
return Class::Number;
}
if let Some(markup) = name.strip_prefix("markup.") {
return match markup.split('.').next() {
Some("heading") => Class::Heading,
Some("link") => Class::Link,
Some("raw") => Class::Code,
Some("quote") => Class::Quote,
Some("list") => Class::List,
_ => Class::Variable,
};
}
let head = name.split('.').next().unwrap_or(name);
match head {
"keyword" => Class::Keyword,
"function" | "constructor" => Class::Function,
"type" | "namespace" | "label" => Class::Type,
"string" | "character" => Class::String,
"comment" => Class::Comment,
"number" | "float" => Class::Number,
"operator" => Class::Operator,
"punctuation" => Class::Punctuation,
"constant" | "boolean" => Class::Constant,
"attribute" | "property" => Class::Attribute,
"tag" => Class::Tag,
_ => Class::Variable,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Span {
pub start: usize,
pub end: usize,
pub class: Class,
pub emphasis: Emphasis,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HighlightError {
Parse,
Cancelled,
InjectionDepth,
}
impl std::fmt::Display for HighlightError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HighlightError::Parse => f.write_str("tree-sitter produced no parse tree"),
HighlightError::Cancelled => f.write_str("syntax analysis superseded"),
HighlightError::InjectionDepth => {
f.write_str("syntax injection nesting exceeds eight levels")
}
}
}
}
impl std::error::Error for HighlightError {}
struct RopeText<'a> {
rope: &'a Rope,
}
impl<'a> TextProvider<&'a [u8]> for RopeText<'a> {
type I = RopeSlices<'a>;
fn text(&mut self, node: tree_sitter::Node<'_>) -> Self::I {
RopeSlices {
rope: self.rope,
start: node.start_byte(),
end: node.end_byte(),
}
}
}
struct RopeSlices<'a> {
rope: &'a Rope,
start: usize,
end: usize,
}
impl<'a> Iterator for RopeSlices<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
if self.start >= self.end {
return None;
}
let (chunk, chunk_start, ..) = self.rope.chunk_at_byte(self.start);
let head = &chunk[self.start - chunk_start..];
let take = head.len().min(self.end - self.start);
let slice = &head.as_bytes()[..take];
self.start += take;
Some(slice)
}
}
pub struct Highlighter {
parser: Parser,
query: Query,
classes: Vec<CaptureStyle>,
source_hash: Option<BufferRevision>,
spans: Vec<Span>,
tree: Option<tree_sitter::Tree>,
tree_revision: BufferRevision,
span_window: Option<(usize, usize)>,
injection_query: Option<Query>,
injection_depth: usize,
children: Vec<injections::InjectedHighlighter>,
}
impl Highlighter {
pub fn invalidate(&mut self) {
self.tree = None;
self.source_hash = None;
self.span_window = None;
self.children.clear();
}
pub fn apply_edits(&mut self, edits: &[strop_core::InputEdit], revision: BufferRevision) {
if revision == self.tree_revision {
return;
}
if let Some(tree) = &mut self.tree {
for edit in edits {
tree.edit(&tree_sitter::InputEdit {
start_byte: edit.start_byte,
old_end_byte: edit.old_end_byte,
new_end_byte: edit.new_end_byte,
start_position: tree_sitter::Point {
row: edit.start_point.0,
column: edit.start_point.1,
},
old_end_position: tree_sitter::Point {
row: edit.old_end_point.0,
column: edit.old_end_point.1,
},
new_end_position: tree_sitter::Point {
row: edit.new_end_point.0,
column: edit.new_end_point.1,
},
});
}
}
self.tree_revision = revision;
for child in &mut self.children {
child.apply_edits(edits, revision);
}
}
pub fn for_path(path: &std::path::Path, rope: &Rope) -> Option<Self> {
let spec = languages::detect(path, Some(&first_line_bounded(rope)))?;
Self::from_spec(spec)
}
fn from_spec(spec: languages::LanguageSpec) -> Option<Self> {
let mut parser = Parser::new();
parser.set_language(&spec.language).ok()?;
let query = Query::new(&spec.language, spec.highlights).ok()?;
let injection_query = if spec.injections.is_empty() {
None
} else {
Some(Query::new(&spec.language, spec.injections).ok()?)
};
let classes = query
.capture_names()
.iter()
.map(|name| CaptureStyle {
class: Class::from_capture(name),
emphasis: Emphasis::from_capture(name),
})
.collect();
Some(Self {
parser,
query,
classes,
source_hash: None,
spans: Vec::new(),
tree: None,
tree_revision: BufferRevision::new(0),
span_window: None,
injection_query,
injection_depth: 0,
children: Vec::new(),
})
}
pub fn highlight(
&mut self,
rope: &Rope,
revision: BufferRevision,
first_byte: usize,
last_byte: usize,
) -> Result<Vec<Span>, HighlightError> {
self.highlight_while(rope, revision, first_byte, last_byte, || false)
}
pub fn highlight_while(
&mut self,
rope: &Rope,
revision: BufferRevision,
first_byte: usize,
last_byte: usize,
cancelled: impl Fn() -> bool,
) -> Result<Vec<Span>, HighlightError> {
self.highlight_cancellable(rope, revision, first_byte, last_byte, &cancelled)
}
fn highlight_cancellable(
&mut self,
rope: &Rope,
revision: BufferRevision,
first_byte: usize,
last_byte: usize,
cancelled: &dyn Fn() -> bool,
) -> Result<Vec<Span>, HighlightError> {
if cancelled() {
return Err(HighlightError::Cancelled);
}
if Some(revision) != self.source_hash {
if self.tree_revision != revision {
self.tree = None;
}
let mut progress = |_: &tree_sitter::ParseState| cancelled();
let tree = self.parser.parse_with_options(
&mut |byte: usize, _| {
if byte >= rope.len_bytes() {
return "";
}
let (chunk, start, _, _) = rope.chunk_at_byte(byte);
&chunk[byte - start..]
},
self.tree.as_ref(),
Some(tree_sitter::ParseOptions::new().progress_callback(&mut progress)),
);
let Some(tree) = tree else {
self.parser.reset();
return Err(if cancelled() {
HighlightError::Cancelled
} else {
HighlightError::Parse
});
};
self.tree = Some(tree);
self.tree_revision = revision;
self.source_hash = Some(revision);
self.span_window = None;
}
let window = (
first_byte.min(rope.len_bytes()),
last_byte.min(rope.len_bytes()),
);
if self.span_window != Some(window) {
let tree = self.tree.as_ref().ok_or(HighlightError::Parse)?;
let mut cursor = QueryCursor::new();
cursor.set_byte_range(window.0..window.1);
let mut progress = |_: &tree_sitter::QueryCursorState| cancelled();
let mut captures = Vec::new();
let mut matches = cursor.matches_with_options(
&self.query,
tree.root_node(),
RopeText { rope },
tree_sitter::QueryCursorOptions::new().progress_callback(&mut progress),
);
while let Some(m) = { StreamingIterator::next(&mut matches) } {
if cancelled() {
return Err(HighlightError::Cancelled);
}
for cap in m.captures {
let node = cap.node;
if node.end_byte() <= window.0 || node.start_byte() >= window.1 {
continue;
}
let style = self.classes[cap.index as usize];
captures.push(LayeredSpan {
span: Span {
start: node.start_byte(),
end: node.end_byte(),
class: style.class,
emphasis: style.emphasis,
},
injected: false,
});
}
}
if cancelled() {
return Err(HighlightError::Cancelled);
}
drop(matches);
captures.extend(self.injection_spans(rope, revision, window.0, window.1, cancelled)?);
self.spans = spans::flatten(captures, window.0, window.1);
self.span_window = Some(window);
}
Ok(self.spans.clone())
}
}
fn cut_at_boundary(head: &str, want: usize) -> usize {
let mut take = want.min(head.len());
while take > 0 && !head.is_char_boundary(take) {
take -= 1;
}
take
}
fn first_line_bounded(rope: &Rope) -> String {
const CAP: usize = 256;
if rope.len_bytes() == 0 || rope.byte(0) != b'#' {
return String::new();
}
let limit = rope.len_bytes().min(CAP);
let mut line = String::new();
let mut byte = 0;
while byte < limit {
let (chunk, start, ..) = rope.chunk_at_byte(byte);
let head = &chunk[byte - start..];
let stop = head.find('\n').unwrap_or(head.len());
let take = cut_at_boundary(head, stop.min(limit - byte));
if take == 0 {
break; }
line.push_str(&head[..take]);
if take == stop {
break; }
byte += take;
}
line
}
#[cfg(test)]
mod tests;