perf: avoid eager diff loading and full-file typing work
This commit is contained in:
+601
-128
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user