2082 lines
74 KiB
Rust
2082 lines
74 KiB
Rust
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<u64>,
|
||
path: Option<PathBuf>,
|
||
syntax: Option<&'static LanguageSpec>,
|
||
source: String,
|
||
next_source: String,
|
||
line_starts: Vec<usize>,
|
||
lines: Vec<Vec<SyntaxSpan>>,
|
||
highlighted: Vec<bool>,
|
||
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())),
|
||
);
|
||
}
|
||
|
||
/// 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);
|
||
}
|
||
|
||
fn update_rows(
|
||
&mut self,
|
||
lines: &[String],
|
||
path: Option<&Path>,
|
||
revision: u64,
|
||
visible: &[std::ops::Range<usize>],
|
||
) {
|
||
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::<usize>() + 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<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>],
|
||
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<SyntaxSpan>],
|
||
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<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 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,
|
||
&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`<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);
|
||
}
|
||
}
|