Files
vamp/src/syntax.rs
T

2171 lines
78 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 range in original line bytes; string coverage may include 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<u64>,
path: Option<PathBuf>,
final_newline: bool,
syntax: Option<&'static LanguageSpec>,
source: String,
next_source: String,
line_starts: Vec<usize>,
lines: Vec<Vec<SyntaxSpan>>,
highlighted: Vec<bool>,
string_spans: Vec<Vec<SyntaxSpan>>,
context: Option<LayerContext>,
parser: Parser,
tree: Option<Tree>,
injection_parser: Parser,
injections: Vec<Layer>,
cursor: QueryCursor,
captures: Vec<Capture>,
highlights: Vec<Highlight>,
events: Vec<(usize, bool, usize)>,
error: Option<String>,
#[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<String> = 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())),
false,
);
}
/// 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<usize>],
) {
self.update_rows(lines, path, revision, visible, false);
}
/// Diff sources store a final line break separately instead of a trailing empty row.
pub(crate) fn update_diff(
&mut self,
lines: &[String],
path: Option<&Path>,
revision: u64,
final_newline: bool,
visible: Option<&[std::ops::Range<usize>]>,
) {
let all_rows = 0..lines.len();
self.update_rows(
lines,
path,
revision,
visible.unwrap_or(std::slice::from_ref(&all_rows)),
final_newline && !lines.is_empty(),
);
}
fn update_rows(
&mut self,
lines: &[String],
path: Option<&Path>,
revision: u64,
visible: &[std::ops::Range<usize>],
final_newline: bool,
) {
let path_changed = self.path.as_deref() != path;
let revision_changed =
self.revision != Some(revision) || self.final_newline != final_newline;
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);
self.final_newline = final_newline;
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::<usize>()
+ lines.len().saturating_sub(1)
+ usize::from(final_newline);
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 final_newline {
self.next_source.push('\n');
}
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;
}
if final_newline {
self.line_starts.push(self.source.len());
}
self.lines.resize_with(lines.len(), Vec::new);
self.string_spans.resize_with(lines.len(), Vec::new);
self.highlighted.resize(lines.len(), false);
}
if edit.is_some() || language_changed {
for spans in self.lines.iter_mut().chain(&mut self.string_spans) {
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 if final_newline {
Point::new(lines.len(), 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 self.lines.iter_mut().chain(&mut self.string_spans) {
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)
}
/// String coverage before foreground overrides, including embedded-language highlighting.
/// An included line break has end_byte == line.len() + 1; foreground spans exclude it.
pub(crate) fn string_spans(&self, row: usize) -> &[SyntaxSpan] {
self.string_spans.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,
&mut self.string_spans,
&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<Vec<(&'static LanguageSpec, Result<Configuration, String>)>> =
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<Configuration, String> {
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<SyntaxKind> {
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<Range>,
depth: usize,
dirty: bool,
context: Option<LayerContext>,
}
#[derive(Clone)]
struct Injection {
syntax: &'static LanguageSpec,
parent: &'static LanguageSpec,
ranges: Vec<Range>,
depth: usize,
ancestors: Vec<(&'static LanguageSpec, Vec<Range>)>,
}
struct LayerContext {
captures: Vec<Capture>,
injections: Vec<Injection>,
}
#[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<SyntaxKind>,
depth: usize,
priority: i32,
pattern: usize,
}
struct Definition {
value_end: usize,
highlight: Option<SyntaxKind>,
}
struct Scope<'a> {
end: usize,
inherits: bool,
definitions: HashMap<&'a str, Vec<Definition>>,
}
#[allow(clippy::too_many_arguments)]
fn collect_layer(
configuration: &Configuration,
tree: &Tree,
source: &str,
ranges: &[Range],
visible: &[Range],
context: &mut Option<LayerContext>,
depth: usize,
parent: Option<&'static LanguageSpec>,
cursor: &mut QueryCursor,
captures: &mut Vec<Capture>,
highlights: &mut Vec<Highlight>,
) -> Result<Vec<Injection>, 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<usize>>,
cursor: &mut QueryCursor,
captures: &mut Vec<Capture>,
) -> 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<Range> {
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<Range>) {
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<SyntaxSpan>],
strings: &mut [Vec<SyntaxSpan>],
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;
let mut active_strings = 0;
for &(byte, entering, index) in events.iter() {
if previous < byte {
let kind = active
.last()
.and_then(|(_, current)| highlights[*current].kind);
if kind.is_some() || active_strings > 0 {
let start = visible.partition_point(|range| range.end_byte <= previous);
for range in visible[start..]
.iter()
.take_while(|range| range.start_byte < byte)
{
let low = previous.max(range.start_byte);
let high = byte.min(range.end_byte);
if let Some(kind) = kind {
append_global(source, starts, lines, low, high, kind, false);
}
if active_strings > 0 {
append_global(source, starts, strings, low, high, SyntaxKind::String, true);
}
}
}
}
if highlights[index].kind == Some(SyntaxKind::String) {
if entering {
active_strings += 1;
} else {
active_strings -= 1;
}
}
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<SyntaxSpan>],
mut start: usize,
end: usize,
kind: SyntaxKind,
include_newline: bool,
) {
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 + usize::from(include_newline && row + 1 < starts.len()));
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<String> {
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 string_newlines_remain_literal_across_visible_row_caching_and_edits() {
let source = "text = \"\"\"a\n\nb\"\"\"\nplain\n";
let input = lines(source);
let path = Path::new("strings.py");
let mut cache = SyntaxCache::new();
let includes_newline = |cache: &SyntaxCache, row: usize, length: usize| {
cache
.string_spans(row)
.iter()
.any(|span| span.start_byte <= length && span.end_byte > length)
};
cache.update_visible(&input, Some(path), 0, std::slice::from_ref(&(0..1)));
assert!(includes_newline(&cache, 0, input[0].len()));
cache.update_visible(&input, Some(path), 0, std::slice::from_ref(&(1..4)));
assert!(includes_newline(&cache, 0, input[0].len()));
assert!(includes_newline(&cache, 1, input[1].len()));
assert!(!includes_newline(&cache, 2, input[2].len()));
assert!(!includes_newline(&cache, 3, input[3].len()));
for (row, line) in input.iter().enumerate() {
assert!(
cache
.line(row)
.iter()
.all(|span| span.end_byte <= line.len())
);
}
cache.update(&lines("# a\n\nb\nplain\n"), Some(path), 1);
assert!(!includes_newline(&cache, 0, 3));
assert!(!includes_newline(&cache, 1, 0));
}
#[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<script>\nfunction nested() {\n return 42;\n}\n</script>\n```",
),
(
"a.html",
"<div>é</div>\n<script>\nconst action = () => 42;\naction;\n</script>\n<style>\nbody { width: 7px; }\n</style>",
),
(
"a.js",
"const page = html`<div>${value}</div>\n<script>\nfunction inside() { return 42; }\n</script>`;",
),
];
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 = <div title=\"hi\" />;\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<SyntaxKind> {
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",
"<div title=\"hi\">text</div>",
"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 = <div title=\"hi\" />;";
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 = "<div>hello</div>\n<script>function hello() { return 42; }</script>\n<style>body { color: red; width: 7px; }</style>";
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("<div>hello", "<div>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,
&mut vec![Vec::new(); 1],
&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,
&mut vec![Vec::new(); 3],
&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,
&mut vec![Vec::new(); 3],
&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`<div>${value}</div><script>function inside() { return 42; }</script>`;";
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<script>function nested() { return 7; }</script>\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);
}
}