1use streaming_iterator::StreamingIterator;
12mod guides;
13pub mod languages;
14pub use guides::{GuideFrame, IndentGuides};
15mod injections;
16mod spans;
17pub use spans::Emphasis;
18use spans::{CaptureStyle, LayeredSpan};
19
20use ropey::Rope;
21use strop_core::id::BufferRevision;
22use tree_sitter::{Parser, Query, QueryCursor, TextProvider};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
27pub enum Class {
28 Keyword,
29 Function,
30 Type,
31 String,
32 Comment,
33 Number,
34 Operator,
35 Punctuation,
36 Constant,
37 Variable,
38 Attribute,
39 Heading,
40 Link,
41 Code,
42 Quote,
43 List,
44 Tag,
45}
46
47impl Class {
48 fn from_capture(name: &str) -> Self {
49 if name.starts_with("constant.numeric.") {
50 return Class::Number;
51 }
52 if let Some(markup) = name.strip_prefix("markup.") {
53 return match markup.split('.').next() {
54 Some("heading") => Class::Heading,
55 Some("link") => Class::Link,
56 Some("raw") => Class::Code,
57 Some("quote") => Class::Quote,
58 Some("list") => Class::List,
59 _ => Class::Variable,
60 };
61 }
62 let head = name.split('.').next().unwrap_or(name);
63 match head {
64 "keyword" => Class::Keyword,
65 "function" | "constructor" => Class::Function,
66 "type" | "namespace" | "label" => Class::Type,
67 "string" | "character" => Class::String,
68 "comment" => Class::Comment,
69 "number" | "float" => Class::Number,
70 "operator" => Class::Operator,
71 "punctuation" => Class::Punctuation,
72 "constant" | "boolean" => Class::Constant,
73 "attribute" | "property" => Class::Attribute,
74 "tag" => Class::Tag,
75 _ => Class::Variable,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
82pub struct Span {
83 pub start: usize,
84 pub end: usize,
85 pub class: Class,
86 pub emphasis: Emphasis,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum HighlightError {
94 Parse,
98 Cancelled,
100 InjectionDepth,
102}
103
104impl std::fmt::Display for HighlightError {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 match self {
107 HighlightError::Parse => f.write_str("tree-sitter produced no parse tree"),
108 HighlightError::Cancelled => f.write_str("syntax analysis superseded"),
109 HighlightError::InjectionDepth => {
110 f.write_str("syntax injection nesting exceeds eight levels")
111 }
112 }
113 }
114}
115
116impl std::error::Error for HighlightError {}
117
118struct RopeText<'a> {
123 rope: &'a Rope,
124}
125
126impl<'a> TextProvider<&'a [u8]> for RopeText<'a> {
127 type I = RopeSlices<'a>;
128
129 fn text(&mut self, node: tree_sitter::Node<'_>) -> Self::I {
130 RopeSlices {
131 rope: self.rope,
132 start: node.start_byte(),
133 end: node.end_byte(),
134 }
135 }
136}
137
138struct RopeSlices<'a> {
142 rope: &'a Rope,
143 start: usize,
144 end: usize,
145}
146
147impl<'a> Iterator for RopeSlices<'a> {
148 type Item = &'a [u8];
149
150 fn next(&mut self) -> Option<Self::Item> {
151 if self.start >= self.end {
152 return None;
153 }
154 let (chunk, chunk_start, ..) = self.rope.chunk_at_byte(self.start);
155 let head = &chunk[self.start - chunk_start..];
156 let take = head.len().min(self.end - self.start);
160 let slice = &head.as_bytes()[..take];
161 self.start += take;
162 Some(slice)
163 }
164}
165
166pub struct Highlighter {
171 parser: Parser,
172 query: Query,
173 classes: Vec<CaptureStyle>,
175 source_hash: Option<BufferRevision>,
176 spans: Vec<Span>,
177 tree: Option<tree_sitter::Tree>,
179 tree_revision: BufferRevision,
180 span_window: Option<(usize, usize)>,
181 injection_query: Option<Query>,
182 injection_depth: usize,
183 children: Vec<injections::InjectedHighlighter>,
184}
185
186impl Highlighter {
187 pub fn invalidate(&mut self) {
190 self.tree = None;
191 self.source_hash = None;
192 self.span_window = None;
193 self.children.clear();
194 }
195
196 pub fn apply_edits(&mut self, edits: &[strop_core::InputEdit], revision: BufferRevision) {
202 if revision == self.tree_revision {
203 return;
204 }
205 if let Some(tree) = &mut self.tree {
206 for edit in edits {
207 tree.edit(&tree_sitter::InputEdit {
208 start_byte: edit.start_byte,
209 old_end_byte: edit.old_end_byte,
210 new_end_byte: edit.new_end_byte,
211 start_position: tree_sitter::Point {
212 row: edit.start_point.0,
213 column: edit.start_point.1,
214 },
215 old_end_position: tree_sitter::Point {
216 row: edit.old_end_point.0,
217 column: edit.old_end_point.1,
218 },
219 new_end_position: tree_sitter::Point {
220 row: edit.new_end_point.0,
221 column: edit.new_end_point.1,
222 },
223 });
224 }
225 }
226 self.tree_revision = revision;
229 for child in &mut self.children {
230 child.apply_edits(edits, revision);
231 }
232 }
233
234 pub fn for_path(path: &std::path::Path, rope: &Rope) -> Option<Self> {
240 let spec = languages::detect(path, Some(&first_line_bounded(rope)))?;
241 Self::from_spec(spec)
242 }
243
244 fn from_spec(spec: languages::LanguageSpec) -> Option<Self> {
245 let mut parser = Parser::new();
246 parser.set_language(&spec.language).ok()?;
247 let query = Query::new(&spec.language, spec.highlights).ok()?;
248 let injection_query = if spec.injections.is_empty() {
249 None
250 } else {
251 Some(Query::new(&spec.language, spec.injections).ok()?)
252 };
253 let classes = query
254 .capture_names()
255 .iter()
256 .map(|name| CaptureStyle {
257 class: Class::from_capture(name),
258 emphasis: Emphasis::from_capture(name),
259 })
260 .collect();
261 Some(Self {
262 parser,
263 query,
264 classes,
265 source_hash: None,
266 spans: Vec::new(),
267 tree: None,
268 tree_revision: BufferRevision::new(0),
269 span_window: None,
270 injection_query,
271 injection_depth: 0,
272 children: Vec::new(),
273 })
274 }
275
276 pub fn highlight(
283 &mut self,
284 rope: &Rope,
285 revision: BufferRevision,
286 first_byte: usize,
287 last_byte: usize,
288 ) -> Result<Vec<Span>, HighlightError> {
289 self.highlight_while(rope, revision, first_byte, last_byte, || false)
290 }
291
292 pub fn highlight_while(
293 &mut self,
294 rope: &Rope,
295 revision: BufferRevision,
296 first_byte: usize,
297 last_byte: usize,
298 cancelled: impl Fn() -> bool,
299 ) -> Result<Vec<Span>, HighlightError> {
300 self.highlight_cancellable(rope, revision, first_byte, last_byte, &cancelled)
301 }
302
303 fn highlight_cancellable(
304 &mut self,
305 rope: &Rope,
306 revision: BufferRevision,
307 first_byte: usize,
308 last_byte: usize,
309 cancelled: &dyn Fn() -> bool,
310 ) -> Result<Vec<Span>, HighlightError> {
311 if cancelled() {
312 return Err(HighlightError::Cancelled);
313 }
314 if Some(revision) != self.source_hash {
315 if self.tree_revision != revision {
318 self.tree = None;
319 }
320 let mut progress = |_: &tree_sitter::ParseState| cancelled();
321 let tree = self.parser.parse_with_options(
322 &mut |byte: usize, _| {
323 if byte >= rope.len_bytes() {
324 return "";
325 }
326 let (chunk, start, _, _) = rope.chunk_at_byte(byte);
327 &chunk[byte - start..]
328 },
329 self.tree.as_ref(),
330 Some(tree_sitter::ParseOptions::new().progress_callback(&mut progress)),
331 );
332 let Some(tree) = tree else {
333 self.parser.reset();
334 return Err(if cancelled() {
335 HighlightError::Cancelled
336 } else {
337 HighlightError::Parse
338 });
339 };
340 self.tree = Some(tree);
341 self.tree_revision = revision;
342 self.source_hash = Some(revision);
343 self.span_window = None;
344 }
345 let window = (
346 first_byte.min(rope.len_bytes()),
347 last_byte.min(rope.len_bytes()),
348 );
349 if self.span_window != Some(window) {
350 let tree = self.tree.as_ref().ok_or(HighlightError::Parse)?;
351 let mut cursor = QueryCursor::new();
352 cursor.set_byte_range(window.0..window.1);
353 let mut progress = |_: &tree_sitter::QueryCursorState| cancelled();
354 let mut captures = Vec::new();
355 let mut matches = cursor.matches_with_options(
356 &self.query,
357 tree.root_node(),
358 RopeText { rope },
359 tree_sitter::QueryCursorOptions::new().progress_callback(&mut progress),
360 );
361 while let Some(m) = { StreamingIterator::next(&mut matches) } {
362 if cancelled() {
363 return Err(HighlightError::Cancelled);
364 }
365 for cap in m.captures {
366 let node = cap.node;
367 if node.end_byte() <= window.0 || node.start_byte() >= window.1 {
368 continue;
369 }
370 let style = self.classes[cap.index as usize];
371 captures.push(LayeredSpan {
372 span: Span {
373 start: node.start_byte(),
374 end: node.end_byte(),
375 class: style.class,
376 emphasis: style.emphasis,
377 },
378 injected: false,
379 });
380 }
381 }
382 if cancelled() {
383 return Err(HighlightError::Cancelled);
384 }
385 drop(matches);
386 captures.extend(self.injection_spans(rope, revision, window.0, window.1, cancelled)?);
387 self.spans = spans::flatten(captures, window.0, window.1);
388 self.span_window = Some(window);
389 }
390 Ok(self.spans.clone())
391 }
392}
393
394fn cut_at_boundary(head: &str, want: usize) -> usize {
398 let mut take = want.min(head.len());
399 while take > 0 && !head.is_char_boundary(take) {
400 take -= 1;
401 }
402 take
403}
404
405fn first_line_bounded(rope: &Rope) -> String {
410 const CAP: usize = 256;
411 if rope.len_bytes() == 0 || rope.byte(0) != b'#' {
413 return String::new();
414 }
415 let limit = rope.len_bytes().min(CAP);
416 let mut line = String::new();
417 let mut byte = 0;
418 while byte < limit {
419 let (chunk, start, ..) = rope.chunk_at_byte(byte);
420 let head = &chunk[byte - start..];
421 let stop = head.find('\n').unwrap_or(head.len());
422 let take = cut_at_boundary(head, stop.min(limit - byte));
423 if take == 0 {
424 break; }
426 line.push_str(&head[..take]);
427 if take == stop {
428 break; }
430 byte += take;
431 }
432 line
433}
434
435#[cfg(test)]
436mod tests;