perf: avoid eager diff loading and full-file typing work

This commit is contained in:
2026-10-11 15:06:05 +02:00
parent 7b4bc80538
commit 2e05725a38
11 changed files with 1881 additions and 386 deletions
+601 -128
View File
@@ -42,6 +42,8 @@ pub struct SyntaxCache {
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,
@@ -65,7 +67,10 @@ impl SyntaxCache {
}
pub(crate) fn update_text(&mut self, text: &str, path: &Path) {
if self.source == text && self.path.as_deref() == Some(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();
@@ -78,13 +83,41 @@ impl SyntaxCache {
/// `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;
if self.revision == Some(revision) && !path_changed {
return;
}
let syntax = languages::detect(path, lines.first().map(String::as_str));
let language_changed = !same_language(self.syntax, syntax);
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);
@@ -114,30 +147,76 @@ impl SyntaxCache {
start += line.len() + 1;
}
self.lines.resize_with(lines.len(), Vec::new);
self.highlighted.resize(lines.len(), false);
}
if edit.is_none() && !language_changed && self.error.is_none() {
return;
if edit.is_some() || language_changed {
for spans in &mut self.lines {
spans.clear();
}
self.highlighted.fill(false);
self.context = None;
}
for spans in &mut self.lines {
spans.clear();
}
self.error = 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) {
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);
}
}
}
@@ -166,6 +245,7 @@ impl SyntaxCache {
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() {
@@ -194,15 +274,19 @@ impl SyntaxCache {
start_byte: 0,
end_byte: self.source.len(),
start_point: Point::new(0, 0),
end_point: point_at(&self.source, self.source.len()),
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();
// ponytail: query the whole incremental tree; restrict changed ranges if query time dominates.
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,
@@ -220,6 +304,7 @@ impl SyntaxCache {
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() {
@@ -250,8 +335,9 @@ impl SyntaxCache {
let unchanged = old
.as_ref()
.is_some_and(|layer| !layer.dirty && layer.ranges == request.ranges);
let tree = if unchanged {
old.unwrap().tree
let (tree, mut context) = if unchanged {
let old = old.unwrap();
(old.tree, old.context)
} else {
self.injection_parser
.set_language(&request.syntax.language)
@@ -263,7 +349,8 @@ impl SyntaxCache {
{
self.injection_parses += 1;
}
self.injection_parser
let tree = self
.injection_parser
.parse(
self.source.as_bytes(),
old.as_ref().map(|layer| &layer.tree),
@@ -273,14 +360,19 @@ impl SyntaxCache {
"{} 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 {
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,
@@ -296,6 +388,8 @@ impl SyntaxCache {
&tree,
&self.source,
&request.ranges,
visible,
&mut context,
request.depth,
Some(request.parent),
&mut self.cursor,
@@ -314,6 +408,7 @@ impl SyntaxCache {
ranges: request.ranges,
depth: request.depth,
dirty: false,
context,
});
}
render_spans(
@@ -322,6 +417,7 @@ impl SyntaxCache {
&mut self.lines,
&self.highlights,
&mut self.events,
visible,
);
Ok(())
}
@@ -380,10 +476,39 @@ fn point_at(source: &str, byte: usize) -> Point {
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(|| {
@@ -429,12 +554,12 @@ fn compile_configuration(syntax: &'static LanguageSpec) -> Result<Configuration,
));
}
}
Ok(Configuration {
Ok(Configuration::new(
syntax,
query,
locals_start,
highlights_start,
})
))
}
fn classify(capture: &str, numeric_literal: bool) -> Option<SyntaxKind> {
@@ -463,8 +588,10 @@ struct Layer {
ranges: Vec<Range>,
depth: usize,
dirty: bool,
context: Option<LayerContext>,
}
#[derive(Clone)]
struct Injection {
syntax: &'static LanguageSpec,
parent: &'static LanguageSpec,
@@ -473,7 +600,12 @@ struct Injection {
ancestors: Vec<(&'static LanguageSpec, Vec<Range>)>,
}
#[derive(Clone, Copy)]
struct LayerContext {
captures: Vec<Capture>,
injections: Vec<Injection>,
}
#[derive(Clone, Copy, Eq, PartialEq)]
struct Capture {
node: usize,
start: usize,
@@ -484,6 +616,34 @@ struct Capture {
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,
@@ -510,6 +670,8 @@ fn collect_layer(
tree: &Tree,
source: &str,
ranges: &[Range],
visible: &[Range],
context: &mut Option<LayerContext>,
depth: usize,
parent: Option<&'static LanguageSpec>,
cursor: &mut QueryCursor,
@@ -519,102 +681,128 @@ fn collect_layer(
let query = &configuration.query;
let names = query.capture_names();
captures.clear();
let mut injections = Vec::new();
let mut combined: HashMap<(usize, usize), usize> = HashMap::new();
let mut matches = cursor.matches(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());
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())],
});
}
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 {
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"
),
});
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(),
});
}
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);
}
collect_highlight_captures(configuration, tree, source, visible, None, cursor, captures)?;
captures.sort_unstable_by_key(|capture| {
(
capture.start,
@@ -624,6 +812,7 @@ fn collect_layer(
capture.capture,
)
});
captures.dedup();
let mut scopes = vec![Scope {
end: usize::MAX,
inherits: false,
@@ -737,7 +926,7 @@ fn collect_layer(
{
let low = first.start.max(range.start_byte);
let high = first.end.min(range.end_byte);
if low < high {
if low < high && intersects(visible, low, high) {
highlights.push(Highlight {
start: low,
end: high,
@@ -754,6 +943,57 @@ fn collect_layer(
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)
@@ -851,6 +1091,7 @@ fn render_spans(
lines: &mut [Vec<SyntaxSpan>],
highlights: &[Highlight],
events: &mut Vec<(usize, bool, usize)>,
visible: &[Range],
) {
events.clear();
for (index, highlight) in highlights.iter().enumerate() {
@@ -875,7 +1116,20 @@ fn render_spans(
&& let Some((_, current)) = active.last()
&& let Some(kind) = highlights[*current].kind
{
append_global(source, starts, lines, previous, byte, 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));
@@ -933,6 +1187,179 @@ mod tests {
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);
@@ -1433,7 +1860,14 @@ mod tests {
pattern: 0,
},
];
render_spans(source, &[0], &mut spans, &highlights, &mut events);
render_spans(
source,
&[0],
&mut spans,
&highlights,
&mut events,
&[source_range(source)],
);
assert_eq!(
spans[0],
vec![
@@ -1472,12 +1906,7 @@ mod tests {
let highlights_start = (0..query.pattern_count())
.filter(|pattern| query.start_byte_for_pattern(*pattern) < locals.len() + 1)
.count();
let configuration = Configuration {
syntax,
query,
locals_start: 0,
highlights_start,
};
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();
@@ -1485,12 +1914,9 @@ mod tests {
&configuration,
cache.tree.as_ref().unwrap(),
source,
&[Range {
start_byte: 0,
end_byte: source.len(),
start_point: Point::new(0, 0),
end_point: point_at(source, source.len()),
}],
&[source_range(source)],
&[source_range(source)],
&mut None,
0,
None,
&mut QueryCursor::new(),
@@ -1504,7 +1930,14 @@ mod tests {
source.find("\nvalue;").unwrap() + 1,
source.find("\nfunction").unwrap() + 1,
];
render_spans(source, &starts, &mut spans, &highlights, &mut Vec::new());
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)
@@ -1514,6 +1947,46 @@ mod tests {
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]