use std::cmp::Reverse; use std::collections::{BTreeSet, HashMap, VecDeque}; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use tree_sitter::{ InputEdit, Node, Parser, Point, Query, QueryCursor, Range, StreamingIterator, Tree, }; mod languages; use languages::LanguageSpec; pub(crate) fn is_markdown_path(path: &Path) -> bool { languages::detect(Some(path), None).is_some_and(|syntax| syntax.name == "Markdown") } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum SyntaxKind { Comment, String, Keyword, Number, Type, Function, Markup, } /// A syntax foreground range in the original line's bytes, excluding its newline. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct SyntaxSpan { pub start_byte: usize, pub end_byte: usize, pub kind: SyntaxKind, } /// Frontend-owned, per-buffer parser state. Revision and path identify unchanged input. #[derive(Default)] pub struct SyntaxCache { revision: Option, path: Option, syntax: Option<&'static LanguageSpec>, source: String, next_source: String, line_starts: Vec, lines: Vec>, highlighted: Vec, context: Option, parser: Parser, tree: Option, injection_parser: Parser, injections: Vec, cursor: QueryCursor, captures: Vec, highlights: Vec, events: Vec<(usize, bool, usize)>, error: Option, #[cfg(test)] parses: usize, #[cfg(test)] incremental_parses: usize, #[cfg(test)] injection_parses: usize, } impl SyntaxCache { pub fn new() -> Self { Self::default() } pub(crate) fn update_text(&mut self, text: &str, path: &Path) { if self.source == text && self.path.as_deref() == Some(path) && self.highlighted.iter().all(|highlighted| *highlighted) { return; } let lines: Vec = text.split('\n').map(str::to_owned).collect(); self.update( &lines, Some(path), self.revision.unwrap_or(0).wrapping_add(1), ); } /// `revision` must change whenever line content changes; path changes are detected separately. pub fn update(&mut self, lines: &[String], path: Option<&Path>, revision: u64) { self.update_rows( lines, path, revision, std::slice::from_ref(&(0..lines.len())), ); } /// Parse the original full source, but collect colors only for these end-exclusive source rows. /// Unrequested rows may be empty; requesting more rows at the same revision reuses parsed trees. pub(crate) fn update_visible( &mut self, lines: &[String], path: Option<&Path>, revision: u64, visible: &[std::ops::Range], ) { self.update_rows(lines, path, revision, visible); } fn update_rows( &mut self, lines: &[String], path: Option<&Path>, revision: u64, visible: &[std::ops::Range], ) { let path_changed = self.path.as_deref() != path; let revision_changed = self.revision != Some(revision); let syntax = if path_changed || revision_changed { languages::detect(path, lines.first().map(String::as_str)) } else { self.syntax }; let language_changed = !same_language(self.syntax, syntax); self.revision = Some(revision); if path_changed { self.path = path.map(Path::to_path_buf); } self.syntax = syntax; let mut edit = None; if revision_changed { self.next_source.clear(); let length = lines.iter().map(String::len).sum::() + lines.len().saturating_sub(1); self.next_source.reserve(length); for (row, line) in lines.iter().enumerate() { if row > 0 { self.next_source.push('\n'); } self.next_source.push_str(line); } if self.source != self.next_source { edit = Some(minimal_edit(&self.source, &self.next_source)); std::mem::swap(&mut self.source, &mut self.next_source); } self.line_starts.clear(); let mut start = 0; for line in lines { self.line_starts.push(start); start += line.len() + 1; } self.lines.resize_with(lines.len(), Vec::new); self.highlighted.resize(lines.len(), false); } if edit.is_some() || language_changed { for spans in &mut self.lines { spans.clear(); } self.highlighted.fill(false); self.context = None; } if language_changed { self.tree = None; self.injections.clear(); } let mut requested = Vec::new(); for rows in visible { let end = rows.end.min(lines.len()); let mut row = rows.start.min(end); while row < end { if self.highlighted[row] { row += 1; continue; } let start = row; while row < end && !self.highlighted[row] { row += 1; } requested.push(Range { start_byte: self.line_starts[start], end_byte: self .line_starts .get(row) .copied() .unwrap_or(self.source.len()), start_point: Point::new(start, 0), end_point: if row < lines.len() { Point::new(row, 0) } else { Point::new( lines.len().saturating_sub(1), lines.last().map_or(0, String::len), ) }, }); } } normalize_ranges(&mut requested); if requested.is_empty() && edit.is_none() && !language_changed { return; } self.error = None; let Some(syntax) = syntax else { self.tree = None; self.injections.clear(); self.highlighted.fill(true); return; }; if let Err(error) = self.highlight(syntax, edit.as_ref(), language_changed, &requested) { self.error = Some(format!("{} syntax: {error}", syntax.name)); self.tree = None; self.injections.clear(); self.context = None; self.highlighted.fill(false); for spans in &mut self.lines { spans.clear(); } } else { for rows in visible { let end = rows.end.min(lines.len()); self.highlighted[rows.start.min(end)..end].fill(true); } } } /// Sorted, non-overlapping, end-exclusive ranges. Unclassified text uses the normal foreground. pub fn line(&self, row: usize) -> &[SyntaxSpan] { self.lines.get(row).map_or(&[], Vec::as_slice) } /// Bundled grammar name, or `None` for unknown languages and plain text. pub fn language(&self) -> Option<&str> { self.syntax.map(|syntax| syntax.name) } /// Genuine grammar/query failures disable highlighting; ERROR nodes in user text do not. pub fn error(&self) -> Option<&str> { self.error.as_deref() } #[cfg(test)] pub(crate) fn parse_counts(&self) -> (usize, usize) { (self.parses, self.injection_parses) } fn highlight( &mut self, syntax: &'static LanguageSpec, edit: Option<&InputEdit>, language_changed: bool, visible: &[Range], ) -> Result<(), String> { let primary_configuration = configuration(syntax)?; if language_changed || self.tree.is_none() { self.parser .set_language(&syntax.language) .map_err(|error| error.to_string())?; } if let Some(edit) = edit && let Some(tree) = &mut self.tree { tree.edit(edit); } if edit.is_some() || self.tree.is_none() { #[cfg(test)] { self.parses += 1; self.incremental_parses += usize::from(self.tree.is_some()); } self.tree = Some( self.parser .parse(self.source.as_bytes(), self.tree.as_ref()) .ok_or_else(|| "parser stopped without a tree".to_owned())?, ); } let full_range = Range { start_byte: 0, end_byte: self.source.len(), start_point: Point::new(0, 0), end_point: Point::new( self.line_starts.len().saturating_sub(1), self.source.len() - self.line_starts.last().copied().unwrap_or(0), ), }; self.highlights.clear(); let mut queue: VecDeque<_> = collect_layer( primary_configuration, self.tree.as_ref().unwrap(), &self.source, &[full_range], visible, &mut self.context, 0, None, &mut self.cursor, &mut self.captures, &mut self.highlights, )? .into(); let mut previous = std::mem::take(&mut self.injections); for layer in &mut previous { layer.dirty = edit.is_some_and(|edit| { layer.ranges.iter().any(|range| { edit.start_byte <= range.end_byte && edit.old_end_byte >= range.start_byte }) }); if let Some(edit) = edit { layer.tree.edit(edit); layer.ranges = layer.tree.included_ranges(); layer.context = None; } } while let Some(request) = queue.pop_front() { // An identical language/range ancestor is a genuine injection cycle. Keep that host // layer, but do not inject it into itself again (e.g. a grammar's injection.self). if request.ancestors.iter().any(|(spec, ranges)| { std::ptr::eq(*spec, request.syntax) && *ranges == request.ranges }) { continue; } if request.depth > 16 { return Err("injection nesting exceeds 16 language layers".to_owned()); } let configuration = configuration(request.syntax)?; let previous_index = previous .iter() .position(|layer| { std::ptr::eq(layer.syntax, request.syntax) && layer.depth == request.depth && layer.ranges == request.ranges }) .or_else(|| { previous.iter().position(|layer| { std::ptr::eq(layer.syntax, request.syntax) && layer.depth == request.depth }) }); let old = previous_index.map(|index| previous.swap_remove(index)); let unchanged = old .as_ref() .is_some_and(|layer| !layer.dirty && layer.ranges == request.ranges); let (tree, mut context) = if unchanged { let old = old.unwrap(); (old.tree, old.context) } else { self.injection_parser .set_language(&request.syntax.language) .map_err(|error| error.to_string())?; self.injection_parser .set_included_ranges(&request.ranges) .map_err(|error| error.to_string())?; #[cfg(test)] { self.injection_parses += 1; } let tree = self .injection_parser .parse( self.source.as_bytes(), old.as_ref().map(|layer| &layer.tree), ) .ok_or_else(|| { format!( "{} injection parser stopped without a tree", request.syntax.name ) })?; (tree, None) }; // Markdown's inline grammar refines the block grammar, so plain inline text keeps // its enclosing heading/link foreground. Other languages replace host strings/markup. let refines_host = languages::by_name("markdown_inline") .is_some_and(|inline| std::ptr::eq(inline, request.syntax)); if !refines_host { for range in request .ranges .iter() .filter(|range| intersects(visible, range.start_byte, range.end_byte)) { self.highlights.push(Highlight { start: range.start_byte, end: range.end_byte, kind: None, depth: request.depth, priority: i32::MIN, pattern: 0, }); } } let mut children = collect_layer( configuration, &tree, &self.source, &request.ranges, visible, &mut context, request.depth, Some(request.parent), &mut self.cursor, &mut self.captures, &mut self.highlights, )?; let mut ancestors = request.ancestors; ancestors.push((request.syntax, request.ranges.clone())); for child in &mut children { child.ancestors = ancestors.clone(); } queue.extend(children); self.injections.push(Layer { syntax: request.syntax, tree, ranges: request.ranges, depth: request.depth, dirty: false, context, }); } render_spans( &self.source, &self.line_starts, &mut self.lines, &self.highlights, &mut self.events, visible, ); Ok(()) } } fn same_language(left: Option<&LanguageSpec>, right: Option<&LanguageSpec>) -> bool { match (left, right) { (Some(left), Some(right)) => std::ptr::eq(left, right), (None, None) => true, _ => false, } } /// One aggregate edit, including byte columns (not character/display columns). fn minimal_edit(old: &str, new: &str) -> InputEdit { let mut start = old .bytes() .zip(new.bytes()) .take_while(|(a, b)| a == b) .count(); while !old.is_char_boundary(start) || !new.is_char_boundary(start) { start -= 1; } let mut suffix = old.as_bytes()[start..] .iter() .rev() .zip(new.as_bytes()[start..].iter().rev()) .take_while(|(a, b)| a == b) .count(); while !old.is_char_boundary(old.len() - suffix) || !new.is_char_boundary(new.len() - suffix) { suffix -= 1; } let old_end = old.len() - suffix; let new_end = new.len() - suffix; InputEdit { start_byte: start, old_end_byte: old_end, new_end_byte: new_end, start_position: point_at(old, start), old_end_position: point_at(old, old_end), new_end_position: point_at(new, new_end), } } fn point_at(source: &str, byte: usize) -> Point { let prefix = &source.as_bytes()[..byte]; Point::new( prefix.iter().filter(|byte| **byte == b'\n').count(), prefix .iter() .rposition(|byte| *byte == b'\n') .map_or(byte, |last| byte - last - 1), ) } struct Configuration { syntax: &'static LanguageSpec, query: Query, context_query: Query, highlights_query: Query, locals_start: usize, highlights_start: usize, } impl Configuration { fn new( syntax: &'static LanguageSpec, query: Query, locals_start: usize, highlights_start: usize, ) -> Self { let mut context_query = query.deep_clone(); let mut highlights_query = query.deep_clone(); for pattern in 0..query.pattern_count() { if pattern < highlights_start { highlights_query.disable_pattern(pattern); } else { context_query.disable_pattern(pattern); } } Self { syntax, query, context_query, highlights_query, locals_start, highlights_start, } } } fn configuration(syntax: &'static LanguageSpec) -> Result<&'static Configuration, String> { static CONFIGURATIONS: LazyLock)>> = LazyLock::new(|| { languages::all() .map(|syntax| (syntax, compile_configuration(syntax))) .collect() }); let (_, result) = CONFIGURATIONS .iter() .find(|(spec, _)| std::ptr::eq(*spec, syntax)) .ok_or_else(|| format!("missing {} query configuration", syntax.name))?; result.as_ref().map_err(Clone::clone) } fn compile_configuration(syntax: &'static LanguageSpec) -> Result { let source = format!( "{}\n{}\n{}", syntax.injections, syntax.locals, syntax.highlights ); let locals_offset = syntax.injections.len() + 1; let highlights_offset = locals_offset + syntax.locals.len() + 1; let query = Query::new(&syntax.language, &source).map_err(|error| error.to_string())?; let mut locals_start = 0; let mut highlights_start = 0; for pattern in 0..query.pattern_count() { let start = query.start_byte_for_pattern(pattern); locals_start += usize::from(start < locals_offset); highlights_start += usize::from(start < highlights_offset); if let Some(predicate) = query.general_predicates(pattern).first() { return Err(format!( "unsupported query predicate #{}", predicate.operator )); } if let Some((property, _)) = query .property_predicates(pattern) .iter() .find(|(property, _)| property.key.as_ref() != "local") { return Err(format!( "unsupported query property predicate {}", property.key )); } } Ok(Configuration::new( syntax, query, locals_start, highlights_start, )) } fn classify(capture: &str, numeric_literal: bool) -> Option { let mut parts = capture.split('.'); match parts.next()? { "comment" => Some(SyntaxKind::Comment), "string" | "character" => Some(SyntaxKind::String), "keyword" | "boolean" | "storage" | "conditional" | "repeat" | "include" | "exception" => { Some(SyntaxKind::Keyword) } "number" | "float" => Some(SyntaxKind::Number), "constant" if numeric_literal || parts.any(|part| part == "numeric") => { Some(SyntaxKind::Number) } "constant" => Some(SyntaxKind::Keyword), "type" | "constructor" => Some(SyntaxKind::Type), "function" | "method" => Some(SyntaxKind::Function), "markup" | "tag" | "text" => Some(SyntaxKind::Markup), _ => None, } } struct Layer { syntax: &'static LanguageSpec, tree: Tree, ranges: Vec, depth: usize, dirty: bool, context: Option, } #[derive(Clone)] struct Injection { syntax: &'static LanguageSpec, parent: &'static LanguageSpec, ranges: Vec, depth: usize, ancestors: Vec<(&'static LanguageSpec, Vec)>, } struct LayerContext { captures: Vec, injections: Vec, } #[derive(Clone, Copy, Eq, PartialEq)] struct Capture { node: usize, start: usize, end: usize, capture: u32, pattern: usize, value_end: usize, numeric_literal: bool, } impl Capture { fn new( capture: &tree_sitter::QueryCapture<'_>, pattern: usize, value_end: usize, name: &str, ) -> Self { Self { node: capture.node.id(), start: capture.node.start_byte(), end: capture.node.end_byte(), capture: capture.index, pattern, value_end, numeric_literal: name.starts_with("constant") && matches!( capture.node.kind(), "integer_literal" | "float_literal" | "number_literal" | "integer" | "float" | "number" ), } } } struct Highlight { start: usize, end: usize, kind: Option, depth: usize, priority: i32, pattern: usize, } struct Definition { value_end: usize, highlight: Option, } struct Scope<'a> { end: usize, inherits: bool, definitions: HashMap<&'a str, Vec>, } #[allow(clippy::too_many_arguments)] fn collect_layer( configuration: &Configuration, tree: &Tree, source: &str, ranges: &[Range], visible: &[Range], context: &mut Option, depth: usize, parent: Option<&'static LanguageSpec>, cursor: &mut QueryCursor, captures: &mut Vec, highlights: &mut Vec, ) -> Result, String> { let query = &configuration.query; let names = query.capture_names(); captures.clear(); let mut injections; if let Some(context) = context { captures.extend_from_slice(&context.captures); injections = context.injections.clone(); } else { injections = Vec::new(); let mut combined: HashMap<(usize, usize), usize> = HashMap::new(); cursor.set_byte_range(0..source.len()); let mut matches = cursor.matches( &configuration.context_query, tree.root_node(), source.as_bytes(), ); while let Some(found) = matches.next() { let pattern = found.pattern_index; if pattern < configuration.locals_start { let mut language = None; let mut nodes = Vec::new(); for capture in found.captures() { match names[capture.index as usize] { "injection.language" => { language = source[capture.node.byte_range()] .split_whitespace() .next() .and_then(languages::by_name); } "injection.content" => nodes.push(capture.node), _ => {} } } let mut include_children = false; let mut is_combined = false; for property in query.property_settings(pattern) { match property.key.as_ref() { "injection.language" => { language = property.value.as_deref().and_then(languages::by_name); } "injection.self" => language = Some(configuration.syntax), "injection.parent" => language = parent, "injection.include-children" => include_children = true, "injection.combined" => is_combined = true, _ => {} } } if let Some(language) = language { let mut content = injection_ranges(&nodes, ranges, include_children); if !content.is_empty() { let key = (pattern, language as *const LanguageSpec as usize); if is_combined && let Some(index) = combined.get(&key) { let injection: &mut Injection = &mut injections[*index]; injection.ranges.append(&mut content); } else { if is_combined { combined.insert(key, injections.len()); } injections.push(Injection { syntax: language, parent: configuration.syntax, ranges: content, depth: depth + 1, ancestors: vec![(configuration.syntax, ranges.to_vec())], }); } } } continue; } let value_end = found .captures() .iter() .find(|capture| names[capture.index as usize] == "local.definition-value") .map_or(0, |capture| capture.node.end_byte()); for capture in found.captures() { let name = names[capture.index as usize]; if name.starts_with('_') || name == "local.definition-value" { continue; } captures.push(Capture::new(capture, pattern, value_end, name)); } } if cursor.did_exceed_match_limit() { return Err(format!( "{} query exceeded its match limit", configuration.syntax.name )); } for injection in &mut injections { normalize_ranges(&mut injection.ranges); } // Locals must be resolved in source order, including definitions above the viewport. // Query only their nodes for the foreground that references inherit. let definitions: BTreeSet<_> = captures .iter() .filter(|capture| names[capture.capture as usize] == "local.definition") .map(|capture| capture.node) .collect(); let mut definition_ranges: Vec<_> = captures .iter() .filter(|capture| definitions.contains(&capture.node)) .map(|capture| Range { start_byte: capture.start, end_byte: capture.end, start_point: Point::new(0, 0), end_point: Point::new(0, 0), }) .collect(); normalize_ranges(&mut definition_ranges); collect_highlight_captures( configuration, tree, source, &definition_ranges, Some(&definitions), cursor, captures, )?; *context = Some(LayerContext { captures: captures.clone(), injections: injections.clone(), }); } collect_highlight_captures(configuration, tree, source, visible, None, cursor, captures)?; captures.sort_unstable_by_key(|capture| { ( capture.start, Reverse(capture.end), capture.node, capture.pattern, capture.capture, ) }); captures.dedup(); let mut scopes = vec![Scope { end: usize::MAX, inherits: false, definitions: HashMap::new(), }]; let mut start = 0; while start < captures.len() { let first = captures[start]; let mut end = start + 1; while end < captures.len() && captures[end].node == first.node { end += 1; } let group = &captures[start..end]; while scopes.len() > 1 && first.start >= scopes.last().unwrap().end { scopes.pop(); } let mut definition = None; let mut reference = None; let mut is_local = false; for capture in group .iter() .filter(|capture| capture.pattern < configuration.highlights_start) { match names[capture.capture as usize] { "local.scope" => { let inherits = query .property_settings(capture.pattern) .iter() .find(|property| property.key.as_ref() == "local.scope-inherits") .is_none_or(|property| { property .value .as_deref() .is_none_or(|value| value == "true") }); scopes.push(Scope { end: capture.end, inherits, definitions: HashMap::new(), }); } "local.definition" => { is_local = true; definition = Some(capture.value_end); } "local.reference" if definition.is_none() => { let name = &source[capture.start..capture.end]; for scope in scopes.iter().rev() { if let Some(found) = scope.definitions.get(name).and_then(|definitions| { definitions .iter() .rev() .find(|definition| capture.start >= definition.value_end) }) { is_local = true; reference = Some(found.highlight); break; } if !scope.inherits { break; } } } _ => {} } } let selected = group .iter() .filter(|capture| { capture.pattern >= configuration.highlights_start && query.property_predicates(capture.pattern).iter().all( |(property, positive)| { let local = property .capture_id .map_or(is_local, |id| id == capture.capture as usize && is_local); local == *positive }, ) }) .max_by_key(|capture| { ( capture_priority(query, capture), capture.pattern, capture.capture, ) }); let kind = reference.unwrap_or_else(|| { selected.and_then(|capture| { classify(names[capture.capture as usize], capture.numeric_literal) }) }); if let Some(value_end) = definition { let name = &source[first.start..first.end]; scopes .last_mut() .unwrap() .definitions .entry(name) .or_default() .push(Definition { value_end, highlight: kind, }); } if let Some(kind) = kind { let selected = selected.copied().unwrap_or(first); let range_start = ranges.partition_point(|range| range.end_byte <= first.start); for range in ranges[range_start..] .iter() .take_while(|range| range.start_byte < first.end) { let low = first.start.max(range.start_byte); let high = first.end.min(range.end_byte); if low < high && intersects(visible, low, high) { highlights.push(Highlight { start: low, end: high, kind: Some(kind), depth, priority: capture_priority(query, &selected), pattern: selected.pattern, }); } } } start = end; } Ok(injections) } fn intersects(ranges: &[Range], start: usize, end: usize) -> bool { ranges .get(ranges.partition_point(|range| range.end_byte <= start)) .is_some_and(|range| range.start_byte < end) } #[allow(clippy::too_many_arguments)] fn collect_highlight_captures( configuration: &Configuration, tree: &Tree, source: &str, ranges: &[Range], definitions: Option<&BTreeSet>, cursor: &mut QueryCursor, captures: &mut Vec, ) -> Result<(), String> { let names = configuration.query.capture_names(); for range in ranges { if range.start_byte == range.end_byte { continue; } cursor.set_byte_range(range.start_byte..range.end_byte); let mut matches = cursor.matches( &configuration.highlights_query, tree.root_node(), source.as_bytes(), ); while let Some(found) = matches.next() { for capture in found.captures() { let name = names[capture.index as usize]; if name.starts_with('_') || capture.node.end_byte() <= range.start_byte || capture.node.start_byte() >= range.end_byte || definitions .is_some_and(|definitions| !definitions.contains(&capture.node.id())) { continue; } captures.push(Capture::new(capture, found.pattern_index, 0, name)); } } if cursor.did_exceed_match_limit() { return Err(format!( "{} query exceeded its match limit", configuration.syntax.name )); } } Ok(()) } fn capture_priority(query: &Query, capture: &Capture) -> i32 { query .property_settings(capture.pattern) .iter() .rev() .find_map(|property| { if property.key.as_ref() == "priority" && property .capture_id .is_none_or(|id| id == capture.capture as usize) { property.value.as_deref()?.parse().ok() } else { None } }) .unwrap_or(0) } fn injection_ranges(nodes: &[Node<'_>], parents: &[Range], include_children: bool) -> Vec { let mut result = Vec::new(); for node in nodes { let mut start_byte = node.start_byte(); let mut start_point = node.start_position(); let mut cursor = node.walk(); let excluded: Vec<_> = if include_children { Vec::new() } else { // The bundled Markdown queries are Neovim-derived: anonymous punctuation is // injected source, while named children (e.g. block continuations) stay in the host. node.named_children(&mut cursor) .map(|child| child.range()) .collect() }; for excluded in excluded.into_iter().chain(std::iter::once(Range { start_byte: node.end_byte(), end_byte: node.end_byte(), start_point: node.end_position(), end_point: node.end_position(), })) { let parent_start = parents.partition_point(|range| range.end_byte <= start_byte); for parent in parents[parent_start..] .iter() .take_while(|range| range.start_byte < excluded.start_byte) { let low = start_byte.max(parent.start_byte); let high = excluded.start_byte.min(parent.end_byte); if low < high { result.push(Range { start_byte: low, end_byte: high, start_point: if low == start_byte { start_point } else { parent.start_point }, end_point: if high == excluded.start_byte { excluded.start_point } else { parent.end_point }, }); } } start_byte = excluded.end_byte; start_point = excluded.end_point; } } normalize_ranges(&mut result); result } fn normalize_ranges(ranges: &mut Vec) { ranges.sort_unstable_by_key(|range| (range.start_byte, range.end_byte)); let mut length = 0; for index in 0..ranges.len() { let range = ranges[index]; if length > 0 && ranges[length - 1].end_byte >= range.start_byte { let previous = &mut ranges[length - 1]; if previous.end_byte < range.end_byte { previous.end_byte = range.end_byte; previous.end_point = range.end_point; } } else { ranges[length] = range; length += 1; } } ranges.truncate(length); } fn render_spans( source: &str, starts: &[usize], lines: &mut [Vec], highlights: &[Highlight], events: &mut Vec<(usize, bool, usize)>, visible: &[Range], ) { events.clear(); for (index, highlight) in highlights.iter().enumerate() { events.push((highlight.start, true, index)); events.push((highlight.end, false, index)); } events.sort_unstable(); let rank = |index: usize| { let highlight = &highlights[index]; ( highlight.depth, highlight.priority, Reverse(highlight.end - highlight.start), highlight.pattern, index, ) }; let mut active: BTreeSet<(_, usize)> = BTreeSet::new(); let mut previous = 0; for &(byte, entering, index) in events.iter() { if previous < byte && let Some((_, current)) = active.last() && let Some(kind) = highlights[*current].kind { let start = visible.partition_point(|range| range.end_byte <= previous); for range in visible[start..] .iter() .take_while(|range| range.start_byte < byte) { append_global( source, starts, lines, previous.max(range.start_byte), byte.min(range.end_byte), kind, ); } } if entering { active.insert((rank(index), index)); } else { active.remove(&(rank(index), index)); } previous = byte; } } fn append_global( source: &str, starts: &[usize], lines: &mut [Vec], mut start: usize, end: usize, kind: SyntaxKind, ) { if starts.is_empty() { return; } let mut row = starts .partition_point(|offset| *offset <= start) .saturating_sub(1); while start < end && row < lines.len() { let line_end = starts.get(row + 1).map_or(source.len(), |next| next - 1); let high = end.min(line_end); if start < high { let spans = &mut lines[row]; let low = start - starts[row]; let high = high - starts[row]; if let Some(previous) = spans.last_mut() && previous.end_byte == low && previous.kind == kind { previous.end_byte = high; } else { spans.push(SyntaxSpan { start_byte: low, end_byte: high, kind, }); } } row += 1; start = line_end + 1; } } #[cfg(test)] mod tests { use super::*; fn lines(source: &str) -> Vec { source.split('\n').map(str::to_owned).collect() } fn source_range(source: &str) -> Range { Range { start_byte: 0, end_byte: source.len(), start_point: Point::new(0, 0), end_point: point_at(source, source.len()), } } #[test] fn visible_windows_match_full_colors_after_edits_and_scrolling_without_reparsing() { let cases = [ ( "a.rs", "fn main() {\n/* start\ninside Ć© šŸ‘©ā€šŸ’»\nend */\nlet n = 42;\nlet text = \"unfinished", ), ("a.py", "text = '''first\ninside Ć©\nlast'''\nanswer = 42"), ( "a.js", "const action = () => 42;\nfunction hello(require) {\n require('local');\n action;\n}\nrequire('global');\naction;", ), ( "a.md", "# Heading **bold**\n\n```rust\nfn main() {\n let n = 42;\n}\n```\n\n```python\ndef hello():\n return 7\n```\n\n```unknown\nfn plain() {}\n```", ), ( "a.md", "> # Heading **bold**\n>\n> ```rust\n> fn main() {\n> let n = 42;\n> }\n> ```", ), ( "a.md", "```html\n\n```", ), ( "a.html", "
Ć©
\n\n", ), ( "a.js", "const page = html`
${value}
\n`;", ), ]; for (path, original) in cases { let mut visible = SyntaxCache::new(); for (revision, source) in [ original.to_owned(), original.replace("42", "123"), format!("\n{original}"), original.replace("() => 42", "42"), ] .iter() .enumerate() { let input = lines(source); let middle = input.len() / 2; visible.update_visible( &input, Some(Path::new(path)), revision as u64, std::slice::from_ref(&(middle..middle + 1)), ); let full = cache(source, path); assert_eq!(visible.error(), None, "{path}"); assert_eq!( visible.line(middle), full.line(middle), "{path} revision {revision}" ); assert!( visible .lines .iter() .enumerate() .all(|(row, spans)| row == middle || spans.is_empty()) ); let parses = visible.parse_counts(); let context = visible.context.as_ref().unwrap().captures.as_ptr(); // Unsorted, overlapping and out-of-bounds windows use original source rows. for row in (0..input.len()).rev() { visible.update_visible( &input, Some(Path::new(path)), revision as u64, &[ row..row + 1, middle..middle + 1, row..row + 1, input.len()..usize::MAX, ], ); assert_eq!( visible.line(row), full.line(row), "{path} revision {revision} row {row}" ); assert_eq!( visible.parse_counts(), parses, "{path}: scrolling reparsed a tree" ); assert_eq!(visible.context.as_ref().unwrap().captures.as_ptr(), context); } } } } #[test] fn visible_highlight_collection_stays_bounded_by_the_requested_rows() { let mut input: Vec<_> = (0..1000) .map(|row| format!("fn function_{row}() {{ let value = 42; }}")) .collect(); let path = Path::new("large.rs"); let mut visible = SyntaxCache::new(); visible.update_visible(&input, Some(path), 0, std::slice::from_ref(&(500..502))); assert_eq!(visible.parses, 1); assert!(visible.captures.len() < 50); assert!(visible.highlights.len() < 30); assert!(visible.lines[..500].iter().all(Vec::is_empty)); assert!(visible.lines[502..].iter().all(Vec::is_empty)); input[500] = "fn changed() { let value = \"Ć© šŸ‘©ā€šŸ’»\"; }".to_owned(); visible.update_visible(&input, Some(path), 1, std::slice::from_ref(&(500..502))); assert_eq!(visible.incremental_parses, 1); assert!(visible.captures.len() < 50); let mut full = SyntaxCache::new(); full.update(&input, Some(path), 1); assert!(full.captures.len() > 1000); for row in 500..502 { assert_eq!(visible.line(row), full.line(row)); } let parses = visible.parse_counts(); visible.update_visible(&input, Some(path), 1, std::slice::from_ref(&(950..952))); assert_eq!(visible.parse_counts(), parses); assert!(visible.captures.len() < 50); for row in 950..952 { assert_eq!(visible.line(row), full.line(row)); } } #[test] fn visible_cache_supports_full_updates_and_same_revision_path_changes() { let input = lines("const element =
;\nconst answer = 42;"); let mut visible = SyntaxCache::new(); visible.update_visible( &input, Some(Path::new("a.ts")), 0, std::slice::from_ref(&(1..2)), ); let parses = visible.parse_counts(); visible.update(&input, Some(Path::new("a.ts")), 0); assert_eq!(visible.parse_counts(), parses); assert_fresh(&visible, &input.join("\n"), "a.ts"); visible.update_visible( &input, Some(Path::new("a.tsx")), 0, std::slice::from_ref(&(0..1)), ); assert_eq!(visible.parses, parses.0 + 1); assert_eq!(visible.line(0), cache(&input.join("\n"), "a.tsx").line(0)); assert!(visible.line(1).is_empty()); visible.update_text(&input.join("\n"), Path::new("a.tsx")); assert_fresh(&visible, &input.join("\n"), "a.tsx"); visible.update_visible( &input, Some(Path::new("a.txt")), 0, std::slice::from_ref(&(0..2)), ); assert_eq!(visible.language(), None); assert!(visible.lines.iter().all(Vec::is_empty)); } fn cache(source: &str, path: &str) -> SyntaxCache { let mut cache = SyntaxCache::new(); cache.update(&lines(source), Some(Path::new(path)), 0); assert_eq!(cache.error(), None, "{path}"); cache } fn kind_at(cache: &SyntaxCache, row: usize, byte: usize) -> Option { cache .line(row) .iter() .find_map(|span| (span.start_byte <= byte && byte < span.end_byte).then_some(span.kind)) } fn assert_token(cache: &SyntaxCache, source: &str, row: usize, token: &str, kind: SyntaxKind) { let line = source.split('\n').nth(row).unwrap(); let start = line.find(token).unwrap(); for (byte, _) in line[start..start + token.len()].char_indices() { assert_eq!( kind_at(cache, row, start + byte), Some(kind), "{token:?} in {}", cache.language().unwrap() ); } } fn assert_fresh(cache: &SyntaxCache, source: &str, path: &str) { let fresh = self::cache(source, path); assert_eq!(cache.language(), fresh.language()); assert_eq!(cache.error(), fresh.error()); assert_eq!( cache.tree.as_ref().map(|tree| tree.root_node().to_sexp()), fresh.tree.as_ref().map(|tree| tree.root_node().to_sexp()) ); let source = lines(source); for (row, line) in source.iter().enumerate() { assert_eq!(cache.line(row), fresh.line(row), "row {row}"); let mut previous = 0; for span in cache.line(row) { assert!(previous <= span.start_byte && span.start_byte < span.end_byte); assert!(span.end_byte <= line.len()); assert!( line.is_char_boundary(span.start_byte) && line.is_char_boundary(span.end_byte) ); previous = span.end_byte; } } } #[test] fn every_bundled_query_compiles_and_common_languages_color_real_tokens() { for language in languages::all() { configuration(language).unwrap_or_else(|error| panic!("{}: {error}", language.name)); } let cases = [ ( "a.rs", "Rust", "fn main() { let x = 42; let s = \"hi\"; /* note */ }", "fn", SyntaxKind::Keyword, ), ( "a.rb", "Ruby", "def hello\n 42\nend", "def", SyntaxKind::Keyword, ), ( "a.py", "Python", "def hello():\n return 42", "def", SyntaxKind::Keyword, ), ( "a.json", "JSON", "{\"n\":42,\"yes\":true}", "42", SyntaxKind::Number, ), ( "a.sh", "Bash", "echo \"hi\" # note", "hi", SyntaxKind::String, ), ( "Makefile", "Makefile", "# note\nall:\n\techo hi", "note", SyntaxKind::Comment, ), ( "a.md", "Markdown", "# Heading\n\n**bold**", "Heading", SyntaxKind::Markup, ), ( "a.js", "JavaScript", "function hello() { return 42; }", "function", SyntaxKind::Keyword, ), ( "a.ts", "TypeScript", "function hello(): number { return 42; }", "function", SyntaxKind::Keyword, ), ( "a.html", "HTML", "
text
", "div", SyntaxKind::Markup, ), ( "a.css", "CSS", "body { width: 42px; }", "42", SyntaxKind::Number, ), ( "a.yaml", "YAML", "message: \"hi\"\ncount: 42", "hi", SyntaxKind::String, ), ( "Cargo.toml", "TOML", "message = \"hi\"\ncount = 42", "hi", SyntaxKind::String, ), ( "a.c", "C", "int main(void) { return 42; }", "return", SyntaxKind::Keyword, ), ( "a.cpp", "C++", "int main() { return 42; }", "return", SyntaxKind::Keyword, ), ( "a.go", "Go", "package main\nfunc main() { return }", "package", SyntaxKind::Keyword, ), ]; for (path, language, source, token, kind) in cases { let cache = self::cache(source, path); assert_eq!(cache.language(), Some(language)); assert_token(&cache, source, 0, token, kind); assert_fresh(&cache, source, path); } } #[test] fn semantic_colors_and_text_predicates_preserve_the_ui_contract() { let source = "fn main() { let n = 42; let f = 3.25; let b = true; let s = \"hello\"; /* note */ }"; let cache = self::cache(source, "a.rs"); for (token, kind) in [ ("fn", SyntaxKind::Keyword), ("let", SyntaxKind::Keyword), ("main", SyntaxKind::Function), ("42", SyntaxKind::Number), ("3.25", SyntaxKind::Number), ("true", SyntaxKind::Keyword), ("hello", SyntaxKind::String), ("note", SyntaxKind::Comment), ] { assert_token(&cache, source, 0, token, kind); } let source = "const foo = 42; const UPPER = 7; foo; UPPER;"; let cache = self::cache(source, "a.js"); assert_eq!(kind_at(&cache, 0, source.find("foo").unwrap()), None); // JavaScript's bundled constant predicate must not color every identifier. assert_ne!( kind_at(&cache, 0, source.find("foo;").unwrap()), Some(SyntaxKind::Keyword) ); } #[test] fn filenames_shebang_save_as_and_tsx_use_actual_grammar_identity() { let source = lines("\u{feff}#!/usr/bin/env python3\nreturn 42"); let mut cache = SyntaxCache::new(); cache.update(&source, None, 0); assert_eq!(cache.language(), Some("Python")); cache.update(&source, Some(Path::new("a.RS")), 0); assert_eq!(cache.language(), Some("Rust")); assert_eq!(cache.parses, 2); cache.update(&source, Some(Path::new("renamed.rs")), 0); assert_eq!(cache.parses, 2); cache.update(&lines("all:\n\techo hi"), Some(Path::new("GNUmakefile")), 1); assert_eq!(cache.language(), Some("Makefile")); let source = "const element =
;"; cache.update(&lines(source), Some(Path::new("a.ts")), 2); let typescript = cache.syntax.unwrap(); let parses = cache.parses; cache.update(&lines(source), Some(Path::new("a.tsx")), 2); assert_eq!(cache.language(), Some("TypeScript")); assert!(!std::ptr::eq(typescript, cache.syntax.unwrap())); assert_eq!(cache.parses, parses + 1); assert_token(&cache, source, 0, "div", SyntaxKind::Markup); assert_fresh(&cache, source, "a.tsx"); let mut script = SyntaxCache::new(); script.update(&lines("#!/usr/bin/env python3\nreturn 42"), None, 0); assert_eq!(script.language(), Some("Python")); script.update(&lines("#!/usr/bin/env bash\necho \"hi\""), None, 1); assert_eq!(script.language(), Some("Bash")); assert_token( &script, "#!/usr/bin/env bash\necho \"hi\"", 1, "hi", SyntaxKind::String, ); } #[test] fn unknown_and_former_non_common_languages_are_honest_plain_text() { for path in [ "a.unknown", "a.txt", "a.hs", "a.java", "a.php", "a.swift", "a.sql", ] { let cache = self::cache("fn main() { \"hi\"; 42; }", path); assert_eq!(cache.language(), None, "{path}"); assert!(cache.lines.iter().all(Vec::is_empty)); assert_eq!(cache.parses, 0); } } #[test] fn unchanged_revision_and_same_source_do_not_allocate_or_parse() { let mut cache = self::cache("fn main() {}\n", "a.rs"); let source = lines("fn main() {}\n"); let source_pointer = cache.source.as_ptr(); let scratch_pointer = cache.next_source.as_ptr(); let tree_id = cache.tree.as_ref().unwrap().root_node().id(); cache.update(&source, Some(Path::new("a.rs")), 0); assert_eq!(cache.source.as_ptr(), source_pointer); assert_eq!(cache.next_source.as_ptr(), scratch_pointer); assert_eq!(cache.tree.as_ref().unwrap().root_node().id(), tree_id); cache.update(&source, Some(Path::new("a.rs")), 1); cache.update(&source, Some(Path::new("b.rs")), 1); assert_eq!(cache.parses, 1); assert_eq!(cache.incremental_parses, 0); assert_eq!(cache.tree.as_ref().unwrap().root_node().id(), tree_id); assert!(cache.line(1).is_empty()); cache.update(&source, Some(Path::new("b.txt")), 1); assert_eq!(cache.language(), None); assert!(cache.tree.is_none()); assert!(cache.lines.iter().all(Vec::is_empty)); cache.update(&source, Some(Path::new("b.rs")), 1); assert_eq!(cache.parses, 2); assert_eq!(cache.incremental_parses, 0); } #[test] fn unicode_minimal_edits_use_byte_points_and_never_split_codepoints() { let old = "x\n\t\"Ć© šŸ‘©ā€šŸ’»\"\n"; let new = "x\n\t\"ĆŖ šŸ‘©ā€šŸ’»\"\n"; let edit = minimal_edit(old, new); assert_eq!(edit.start_byte, 4); assert_eq!(edit.start_position, Point::new(1, 2)); assert_eq!(edit.old_end_byte, 6); assert_eq!(edit.new_end_byte, 6); assert_eq!(edit.old_end_position, Point::new(1, 4)); let edit = minimal_edit("a\nβ\n", "a\n\nβ\n"); assert_eq!(edit.start_position, Point::new(1, 0)); assert_eq!(edit.old_end_position, Point::new(1, 0)); assert_eq!(edit.new_end_position, Point::new(2, 0)); let edit = minimal_edit("α", ""); assert_eq!( (edit.start_byte, edit.old_end_byte, edit.new_end_byte), (0, 2, 0) ); let edit = minimal_edit("xα", "xѱ"); assert_eq!( (edit.start_byte, edit.old_end_byte, edit.new_end_byte), (1, 3, 3) ); assert_eq!(edit.new_end_position, Point::new(0, 3)); } #[test] fn incremental_unicode_newline_insert_remove_and_undo_match_fresh_trees() { let original = "fn main() {\n\tlet text = \"Ć© é šŸ‘©ā€šŸ’»\";\n\t/* nested /* inner */ note */\n}\n"; let mut cache = self::cache(original, "a.rs"); let edits = [ original.replace("Ć©", "ĆŖ"), original.replace("let text", "\nlet text"), original.replace("\n\t/* nested /* inner */ note */", ""), original.replace("šŸ‘©ā€šŸ’»", "šŸ‘©ā€šŸš€"), original.replace("\";", "\";\n\tlet n = 42;"), original.to_owned(), String::new(), original.to_owned(), ]; for (revision, source) in edits.iter().enumerate() { cache.update(&lines(source), Some(Path::new("a.rs")), revision as u64 + 1); assert_fresh(&cache, source, "a.rs"); } assert_eq!(cache.parses, edits.len() + 1); assert_eq!(cache.incremental_parses, edits.len()); } #[test] fn unfinished_source_and_multiline_offscreen_state_remain_highlighted() { let source = "fn main() {\n/* start\ninside šŸ‘©ā€šŸ’»\nend */\nlet n = 42;\nlet text = \"unfinished"; let cache = self::cache(source, "a.rs"); assert!(cache.tree.as_ref().unwrap().root_node().has_error()); assert_token(&cache, source, 0, "fn", SyntaxKind::Keyword); assert_token(&cache, source, 2, "inside", SyntaxKind::Comment); assert_token(&cache, source, 4, "42", SyntaxKind::Number); assert_eq!(cache.error(), None); let source = "text = '''first\ninside\nlast'''\nanswer = 42"; let cache = self::cache(source, "a.py"); assert_token(&cache, source, 1, "inside", SyntaxKind::String); assert_token(&cache, source, 3, "42", SyntaxKind::Number); } #[test] fn markdown_heading_markers_share_title_colors_without_coloring_plain_hashes() { let source = "# One\n## Two\n### Three\n#### Four\n##### Five\n###### Six\n\nSetext\n======\n\nplain # text"; let cache = self::cache(source, "headings.md"); for row in 0..6 { assert_token( &cache, source, row, &"#".repeat(row + 1), SyntaxKind::Markup, ); } assert_token(&cache, source, 0, "One", SyntaxKind::Markup); assert_token(&cache, source, 7, "Setext", SyntaxKind::Markup); assert_token(&cache, source, 8, "======", SyntaxKind::Markup); assert_eq!(kind_at(&cache, 10, 6), None); } #[test] fn markdown_list_markers_color_bullets_and_numbers_without_coloring_plain_text() { let source = "- item\n - nested\n 12. ordered\n\n+ item\n\n* item\n\n1. ordered\n2. next\n\n3) ordered\n\n> - quoted\n> 4. ordered\n\nplain - text and 1. text\n\n\\- escaped"; let mut cache = self::cache(source, "lists.md"); for (row, marker) in [ (0, "-"), (1, "-"), (2, "12."), (4, "+"), (6, "*"), (8, "1."), (9, "2."), (11, "3)"), (13, "-"), (14, "4."), ] { assert_token(&cache, source, row, marker, SyntaxKind::Markup); } assert_eq!(kind_at(&cache, 0, 2), None); assert_eq!(kind_at(&cache, 16, 6), None); assert_eq!(kind_at(&cache, 16, 17), None); assert_ne!(kind_at(&cache, 18, 1), Some(SyntaxKind::Markup)); let edited = source.replacen("- item", "plain - item", 1); cache.update(&lines(&edited), Some(Path::new("lists.md")), 1); assert_eq!(kind_at(&cache, 0, 6), None); assert_fresh(&cache, &edited, "lists.md"); } #[test] fn markdown_inline_and_fences_inject_known_languages_and_update_incrementally() { let original = "A **bold** and `code` link.\n\n```rust\nfn main() { let n = 42; }\n```\n\n```python\ndef hello():\n return 7\n```\n\n```not-a-language\nfn not_rust() { 99; }\n```"; let mut cache = self::cache(original, "a.md"); assert_token(&cache, original, 0, "bold", SyntaxKind::Markup); assert_token(&cache, original, 0, "code", SyntaxKind::Markup); assert_token(&cache, original, 3, "fn", SyntaxKind::Keyword); assert_token(&cache, original, 3, "main", SyntaxKind::Function); assert_token(&cache, original, 3, "42", SyntaxKind::Number); assert_token(&cache, original, 7, "def", SyntaxKind::Keyword); assert_ne!(kind_at(&cache, 12, 0), Some(SyntaxKind::Keyword)); let parses = cache.injection_parses; let source = original.replace("42", "123"); cache.update(&lines(&source), Some(Path::new("a.md")), 1); assert_token(&cache, &source, 3, "123", SyntaxKind::Number); assert_fresh(&cache, &source, "a.md"); assert!(cache.injection_parses > parses); let source = source.replace("```rust", "```unknown"); cache.update(&lines(&source), Some(Path::new("a.md")), 2); assert_ne!(kind_at(&cache, 3, 0), Some(SyntaxKind::Keyword)); assert_fresh(&cache, &source, "a.md"); } #[test] fn markdown_injections_keep_anonymous_syntax_but_not_named_block_continuations() { let source = "> # Heading **bold**\n>\n> ```rust\n> fn main() {\n> let n = 3.25;\n> }\n> ```"; let cache = self::cache(source, "a.md"); assert_token(&cache, source, 0, "Heading", SyntaxKind::Markup); assert_token(&cache, source, 0, "bold", SyntaxKind::Markup); assert_token(&cache, source, 3, "main", SyntaxKind::Function); assert_token(&cache, source, 4, "3.25", SyntaxKind::Number); let rust = languages::by_name("rust").unwrap(); let layer = cache .injections .iter() .find(|layer| std::ptr::eq(layer.syntax, rust)) .unwrap(); for range in &layer.ranges { assert!(!source[range.start_byte..range.end_byte].contains('>')); } let braces = [source.find('{').unwrap(), source.find('}').unwrap()]; for byte in braces { assert!( layer .ranges .iter() .any(|range| range.start_byte <= byte && byte < range.end_byte) ); } assert_fresh(&cache, source, "a.md"); } #[test] fn html_script_style_injections_override_host_and_reuse_unchanged_trees() { let original = "
hello
\n\n"; let mut cache = self::cache(original, "a.html"); assert_token(&cache, original, 0, "div", SyntaxKind::Markup); assert_token(&cache, original, 1, "function", SyntaxKind::Keyword); assert_token(&cache, original, 1, "hello", SyntaxKind::Function); assert_token(&cache, original, 1, "42", SyntaxKind::Number); assert_token(&cache, original, 2, "7", SyntaxKind::Number); let injection_parses = cache.injection_parses; let source = original.replace("
hello", "
world"); cache.update(&lines(&source), Some(Path::new("a.html")), 1); assert_eq!(cache.injection_parses, injection_parses); assert_fresh(&cache, &source, "a.html"); let source = source.replace("42", "123"); cache.update(&lines(&source), Some(Path::new("a.html")), 2); assert_token(&cache, &source, 1, "123", SyntaxKind::Number); assert_fresh(&cache, &source, "a.html"); // Inserting at an included range's exact end must reparse, not merely edit its old tree. let source = source.replace("return 123; }", "return 123; } function added() {}"); cache.update(&lines(&source), Some(Path::new("a.html")), 3); assert_token(&cache, &source, 1, "added", SyntaxKind::Function); assert_fresh(&cache, &source, "a.html"); } #[test] fn local_definitions_disable_builtin_captures_and_propagate_highlights() { let source = "function hello(require) { require('local'); }\nrequire('global');"; let cache = self::cache(source, "a.js"); let local = source.find("require('local')").unwrap(); assert_eq!(kind_at(&cache, 0, local), None); assert_token(&cache, source, 1, "require", SyntaxKind::Function); let source = "const action = () => 42;\naction;"; let cache = self::cache(source, "a.js"); assert_token(&cache, source, 0, "action", SyntaxKind::Function); assert_token(&cache, source, 1, "action", SyntaxKind::Function); } #[test] fn overlapping_capture_specificity_priority_and_plain_injection_are_nonoverlapping() { let source = "αβγ"; let mut spans = vec![Vec::new()]; let mut events = Vec::new(); let highlights = vec![ Highlight { start: 0, end: 6, kind: Some(SyntaxKind::String), depth: 0, priority: 0, pattern: 99, }, Highlight { start: 2, end: 4, kind: Some(SyntaxKind::Function), depth: 0, priority: 0, pattern: 1, }, Highlight { start: 4, end: 6, kind: None, depth: 1, priority: i32::MIN, pattern: 0, }, Highlight { start: 0, end: 2, kind: Some(SyntaxKind::Keyword), depth: 0, priority: 10, pattern: 0, }, ]; render_spans( source, &[0], &mut spans, &highlights, &mut events, &[source_range(source)], ); assert_eq!( spans[0], vec![ SyntaxSpan { start_byte: 0, end_byte: 2, kind: SyntaxKind::Keyword }, SyntaxSpan { start_byte: 2, end_byte: 4, kind: SyntaxKind::Function }, ] ); } #[test] fn local_initializer_visibility_positive_predicates_and_noninheriting_scopes() { let syntax = languages::by_name("javascript").unwrap(); let locals = r#" (program) @local.scope ((function_declaration) @local.scope (#set! local.scope-inherits false)) (variable_declarator name: (identifier) @local.definition value: (_) @local.definition-value) (identifier) @local.reference "#; let source_query = format!( "{locals}\n{}", r#" ((identifier) @function (#is-not? local)) ((identifier) @string (#is? local)) (variable_declarator name: (identifier) @type) "# ); let query = Query::new(&syntax.language, &source_query).unwrap(); let highlights_start = (0..query.pattern_count()) .filter(|pattern| query.start_byte_for_pattern(*pattern) < locals.len() + 1) .count(); let configuration = Configuration::new(syntax, query, 0, highlights_start); let source = "const value = value;\nvalue;\nfunction inner() { value; }"; let cache = self::cache(source, "a.js"); let mut highlights = Vec::new(); collect_layer( &configuration, cache.tree.as_ref().unwrap(), source, &[source_range(source)], &[source_range(source)], &mut None, 0, None, &mut QueryCursor::new(), &mut Vec::new(), &mut highlights, ) .unwrap(); let mut spans = vec![Vec::new(); 3]; let starts = [ 0, source.find("\nvalue;").unwrap() + 1, source.find("\nfunction").unwrap() + 1, ]; render_spans( source, &starts, &mut spans, &highlights, &mut Vec::new(), &[source_range(source)], ); let kind = |row: usize, byte: usize| { spans[row].iter().find_map(|span| { (span.start_byte <= byte && byte < span.end_byte).then_some(span.kind) }) }; assert_eq!(kind(0, 6), Some(SyntaxKind::Type)); assert_eq!(kind(0, 14), Some(SyntaxKind::Function)); assert_eq!(kind(1, 0), Some(SyntaxKind::Type)); assert_eq!(kind(2, 19), Some(SyntaxKind::Function)); let mut context = None; for row in [1, 2] { let window = [Range { start_byte: starts[row], end_byte: starts.get(row + 1).copied().unwrap_or(source.len()), start_point: Point::new(row, 0), end_point: point_at(source, starts.get(row + 1).copied().unwrap_or(source.len())), }]; let mut visible_highlights = Vec::new(); collect_layer( &configuration, cache.tree.as_ref().unwrap(), source, &[source_range(source)], &window, &mut context, 0, None, &mut QueryCursor::new(), &mut Vec::new(), &mut visible_highlights, ) .unwrap(); let mut visible_spans = vec![Vec::new(); 3]; render_spans( source, &starts, &mut visible_spans, &visible_highlights, &mut Vec::new(), &window, ); assert_eq!(visible_spans[row], spans[row]); assert!( visible_spans .iter() .enumerate() .all(|(other, spans)| other == row || spans.is_empty()) ); } } #[test] fn grammar_and_unsupported_predicate_errors_are_not_silent_empty_queries() { static INVALID: LazyLock<[LanguageSpec; 2]> = LazyLock::new(|| { let language = languages::by_name("rust").unwrap(); [ "(not_a_rust_node) @keyword", "((identifier) @function (#unsupported? @function))", ] .map(|query| LanguageSpec { name: "invalid test grammar query", language: language.language.clone(), highlights: query, injections: "", locals: "", }) }); for syntax in INVALID.iter() { assert!(compile_configuration(syntax).is_err()); } } #[test] fn combined_tagged_templates_and_nested_markdown_html_keep_real_injections() { let source = "const page = html`
${value}
`;"; let cache = self::cache(source, "a.js"); assert_token(&cache, source, 0, "div", SyntaxKind::Markup); assert_token(&cache, source, 0, "inside", SyntaxKind::Function); assert_token(&cache, source, 0, "42", SyntaxKind::Number); assert_fresh(&cache, source, "a.js"); let source = "```html\n\n```"; let cache = self::cache(source, "a.md"); assert_token(&cache, source, 1, "nested", SyntaxKind::Function); assert_token(&cache, source, 1, "7", SyntaxKind::Number); assert_fresh(&cache, source, "a.md"); let source = "fn main() { println!(\"{}\", 42); }"; let cache = self::cache(source, "a.rs"); assert_token(&cache, source, 0, "42", SyntaxKind::Number); assert!(cache.injections.len() <= 16); assert_fresh(&cache, source, "a.rs"); } #[test] fn parser_state_is_isolated_between_buffers() { let mut first = self::cache("/* open\ninside", "a.rs"); let second = self::cache("fn main() { let n = 42; }", "b.rs"); first.update( &lines("/* open */\nfn other() {}"), Some(Path::new("a.rs")), 1, ); assert_token( &second, "fn main() { let n = 42; }", 0, "fn", SyntaxKind::Keyword, ); assert_eq!(second.parses, 1); assert_eq!(first.incremental_parses, 1); } }