Files
vamp/src/syntax.rs
T
hl-valdemar adbd2c3f81 fix: keep large-file editing responsive
Bound synchronous full-source syntax parsing, cache dirty comparisons, edit ordinary inline changes in place, retain changed-row undo history, and buffer atomic saves. Skip repository snapshots beyond existing worker limits before copying their text.
2026-10-16 21:16:53 +02:00

1670 lines
56 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::cell::OnceCell;
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) const RUST_ID: &str = "rust";
pub(crate) const RUBY_ID: &str = "ruby";
pub(crate) const PYTHON_ID: &str = "python";
// Full trees and their indexes are optional; editing remains available beyond these budgets.
const MAX_SYNTAX_BYTES: usize = 1024 * 1024;
const MAX_SYNTAX_LINE_BYTES: usize = 64 * 1024;
const SOURCE_BUDGET_NOTICE: &str = "source exceeds 1 MiB syntax budget";
const LINE_BUDGET_NOTICE: &str = "line exceeds 64 KiB syntax budget";
pub(crate) fn is_markdown_path(path: &Path) -> bool {
languages::detect(Some(path), None).is_some_and(|syntax| syntax.name == "Markdown")
}
pub(crate) fn language_id(path: Option<&Path>, first_line: Option<&str>) -> Option<&'static str> {
languages::detect(path, first_line).map(|syntax| syntax.id)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SyntaxKind {
Comment,
String,
Keyword,
Number,
Type,
Function,
Markup,
}
/// A syntax range in original line bytes; string coverage may include its newline.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SyntaxSpan {
pub start_byte: usize,
pub end_byte: usize,
pub kind: SyntaxKind,
}
/// Innermost named declaration, with its full enclosing path and end-exclusive source rows.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct ScopeContext<'a> {
pub start_row: usize,
pub end_row: usize,
pub name: Point,
pub label: &'a str,
}
struct CachedScope {
start_row: usize,
end_row: usize,
name: Point,
label: String,
}
struct ScopeIndex {
declarations: Vec<CachedScope>,
rows: Vec<Option<usize>>,
}
/// Frontend-owned, per-buffer parser state. Revision and path identify unchanged input.
#[derive(Default)]
pub struct SyntaxCache {
revision: Option<u64>,
path: Option<PathBuf>,
final_newline: bool,
syntax: Option<&'static LanguageSpec>,
budget_exceeded: Option<&'static str>,
source: String,
next_source: String,
line_starts: Vec<usize>,
lines: Vec<Vec<SyntaxSpan>>,
highlighted: Vec<bool>,
string_spans: Vec<Vec<SyntaxSpan>>,
context: Option<LayerContext>,
parser: Parser,
tree: Option<Tree>,
injection_parser: Parser,
injections: Vec<Layer>,
scopes: OnceCell<ScopeIndex>,
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) {
// Check lengths before equality, splitting into owned lines, or copying the source.
let budget_exceeded = if text.len() > MAX_SYNTAX_BYTES {
Some(SOURCE_BUDGET_NOTICE)
} else if text
.split('\n')
.any(|line| line.len() > MAX_SYNTAX_LINE_BYTES)
{
Some(LINE_BUDGET_NOTICE)
} else {
None
};
if let Some(reason) = budget_exceeded {
let syntax = languages::detect(Some(path), None)
.or_else(|| languages::detect(None, Some(syntax_prefix(text))));
let language_changed = !same_language(self.syntax, syntax);
self.revision = None;
self.final_newline = false;
if self.path.as_deref() != Some(path) {
self.path = Some(path.to_path_buf());
}
self.syntax = syntax;
if self.budget_exceeded != Some(reason) || language_changed {
self.use_plain_text(reason);
}
return;
}
if self.budget_exceeded.is_none()
&& self.source == text
&& self.path.as_deref() == Some(path)
&& self.highlighted.iter().all(|highlighted| *highlighted)
{
return;
}
let lines: Vec<String> = text.split('\n').map(str::to_owned).collect();
self.update(
&lines,
Some(path),
self.revision.unwrap_or(0).wrapping_add(1),
);
}
/// `revision` must change whenever line content changes; path changes are detected separately.
pub fn update(&mut self, lines: &[String], path: Option<&Path>, revision: u64) {
self.update_rows(
lines,
path,
revision,
std::slice::from_ref(&(0..lines.len())),
false,
);
}
/// Parse the original full source, but collect colors only for these end-exclusive source rows.
/// Unrequested rows may be empty; requesting more rows at the same revision reuses parsed trees.
pub(crate) fn update_visible(
&mut self,
lines: &[String],
path: Option<&Path>,
revision: u64,
visible: &[std::ops::Range<usize>],
) {
self.update_rows(lines, path, revision, visible, false);
}
/// Diff sources store a final line break separately instead of a trailing empty row.
pub(crate) fn update_diff(
&mut self,
lines: &[String],
path: Option<&Path>,
revision: u64,
final_newline: bool,
visible: Option<&[std::ops::Range<usize>]>,
) {
let all_rows = 0..lines.len();
self.update_rows(
lines,
path,
revision,
visible.unwrap_or(std::slice::from_ref(&all_rows)),
final_newline && !lines.is_empty(),
);
let _ = self.scope_at(0);
}
fn update_rows(
&mut self,
lines: &[String],
path: Option<&Path>,
revision: u64,
visible: &[std::ops::Range<usize>],
final_newline: bool,
) {
let path_changed = self.path.as_deref() != path;
let revision_changed =
self.revision != Some(revision) || self.final_newline != final_newline;
if self.budget_exceeded.is_some() && !path_changed && !revision_changed {
return;
}
let syntax = if path_changed || revision_changed {
languages::detect(path, lines.first().map(|line| syntax_prefix(line)))
} else {
self.syntax
};
let language_changed = !same_language(self.syntax, syntax);
self.revision = Some(revision);
self.final_newline = final_newline;
if path_changed {
self.path = path.map(Path::to_path_buf);
}
self.syntax = syntax;
let length = if revision_changed {
syntax_source_length(lines, final_newline)
} else if let Some(reason) = self.budget_exceeded {
Err(reason)
} else {
Ok(self.source.len())
};
let length = match length {
Ok(length) => length,
Err(reason) => {
if self.budget_exceeded != Some(reason) || language_changed {
self.use_plain_text(reason);
}
return;
}
};
if self.budget_exceeded.take().is_some() {
self.error = None;
}
let mut edit = None;
if revision_changed {
self.next_source.clear();
self.next_source.reserve(length);
for (row, line) in lines.iter().enumerate() {
if row > 0 {
self.next_source.push('\n');
}
self.next_source.push_str(line);
}
if final_newline {
self.next_source.push('\n');
}
if self.source != self.next_source {
edit = Some(minimal_edit(&self.source, &self.next_source));
std::mem::swap(&mut self.source, &mut self.next_source);
}
self.line_starts.clear();
let mut start = 0;
for line in lines {
self.line_starts.push(start);
start += line.len() + 1;
}
if final_newline {
self.line_starts.push(self.source.len());
}
self.lines.resize_with(lines.len(), Vec::new);
self.string_spans.resize_with(lines.len(), Vec::new);
self.highlighted.resize(lines.len(), false);
}
if edit.is_some() || language_changed {
for spans in self.lines.iter_mut().chain(&mut self.string_spans) {
spans.clear();
}
self.highlighted.fill(false);
self.context = None;
self.scopes.take();
}
if language_changed {
self.tree = None;
self.injections.clear();
}
let mut requested = Vec::new();
for rows in visible {
let end = rows.end.min(lines.len());
let mut row = rows.start.min(end);
while row < end {
if self.highlighted[row] {
row += 1;
continue;
}
let start = row;
while row < end && !self.highlighted[row] {
row += 1;
}
requested.push(Range {
start_byte: self.line_starts[start],
end_byte: self
.line_starts
.get(row)
.copied()
.unwrap_or(self.source.len()),
start_point: Point::new(start, 0),
end_point: if row < lines.len() {
Point::new(row, 0)
} else if final_newline {
Point::new(lines.len(), 0)
} else {
Point::new(
lines.len().saturating_sub(1),
lines.last().map_or(0, String::len),
)
},
});
}
}
normalize_ranges(&mut requested);
if requested.is_empty() && edit.is_none() && !language_changed {
return;
}
self.error = None;
let Some(syntax) = syntax else {
self.tree = None;
self.injections.clear();
self.highlighted.fill(true);
return;
};
if let Err(error) = self.highlight(syntax, edit.as_ref(), language_changed, &requested) {
self.error = Some(format!("{} syntax: {error}", syntax.name));
self.tree = None;
self.injections.clear();
self.context = None;
self.scopes.take();
self.highlighted.fill(false);
for spans in self.lines.iter_mut().chain(&mut self.string_spans) {
spans.clear();
}
} else {
for rows in visible {
let end = rows.end.min(lines.len());
self.highlighted[rows.start.min(end)..end].fill(true);
}
}
}
fn use_plain_text(&mut self, reason: &'static str) {
// Drop parser arenas as well as source/span capacities when crossing the budget.
*self = Self {
revision: self.revision,
path: self.path.take(),
final_newline: self.final_newline,
syntax: self.syntax,
budget_exceeded: Some(reason),
error: self
.syntax
.map(|syntax| format!("{}: plain text ({reason})", syntax.name)),
#[cfg(test)]
parses: self.parses,
#[cfg(test)]
incremental_parses: self.incremental_parses,
#[cfg(test)]
injection_parses: self.injection_parses,
..Self::default()
};
}
/// 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)
}
/// Lazily index existing trees; subsequent lookups neither allocate nor reparse.
pub(crate) fn scope_at(&self, row: usize) -> Option<ScopeContext<'_>> {
let syntax = self.syntax?;
let tree = self.tree.as_ref()?;
if row >= self.lines.len()
|| (!has_scope_language(syntax.id)
&& !self
.injections
.iter()
.any(|layer| has_scope_language(layer.syntax.id)))
{
return None;
}
let index = self.scopes.get_or_init(|| {
let mut index = ScopeIndex {
declarations: Vec::new(),
rows: vec![None; self.lines.len()],
};
collect_scopes(
tree.root_node(),
syntax.id,
&self.source,
&[],
&mut index.declarations,
&mut index.rows,
);
for layer in &self.injections {
collect_scopes(
layer.tree.root_node(),
layer.syntax.id,
&self.source,
&layer.ranges,
&mut index.declarations,
&mut index.rows,
);
}
index
});
let scope = &index.declarations[index.rows[row]?];
Some(ScopeContext {
start_row: scope.start_row,
end_row: scope.end_row,
name: scope.name,
label: &scope.label,
})
}
/// String coverage before foreground overrides, including embedded-language highlighting.
/// An included line break has end_byte == line.len() + 1; foreground spans exclude it.
pub(crate) fn string_spans(&self, row: usize) -> &[SyntaxSpan] {
self.string_spans.get(row).map_or(&[], Vec::as_slice)
}
/// Bundled grammar name, or `None` for unknown languages and plain text.
pub fn language(&self) -> Option<&str> {
self.syntax.map(|syntax| syntax.name)
}
/// Why highlighting is unavailable: a deliberate plain-text fallback or grammar/query failure.
pub fn error(&self) -> Option<&str> {
self.error.as_deref()
}
#[cfg(test)]
pub(crate) fn parse_counts(&self) -> (usize, usize) {
(self.parses, self.injection_parses)
}
fn highlight(
&mut self,
syntax: &'static LanguageSpec,
edit: Option<&InputEdit>,
language_changed: bool,
visible: &[Range],
) -> Result<(), String> {
let primary_configuration = configuration(syntax)?;
if language_changed || self.tree.is_none() {
self.parser
.set_language(&syntax.language)
.map_err(|error| error.to_string())?;
}
if let Some(edit) = edit
&& let Some(tree) = &mut self.tree
{
tree.edit(edit);
}
if edit.is_some() || self.tree.is_none() {
#[cfg(test)]
{
self.parses += 1;
self.incremental_parses += usize::from(self.tree.is_some());
}
self.tree = Some(
self.parser
.parse(self.source.as_bytes(), self.tree.as_ref())
.ok_or_else(|| "parser stopped without a tree".to_owned())?,
);
}
let full_range = Range {
start_byte: 0,
end_byte: self.source.len(),
start_point: Point::new(0, 0),
end_point: Point::new(
self.line_starts.len().saturating_sub(1),
self.source.len() - self.line_starts.last().copied().unwrap_or(0),
),
};
self.highlights.clear();
let mut queue: VecDeque<_> = collect_layer(
primary_configuration,
self.tree.as_ref().unwrap(),
&self.source,
&[full_range],
visible,
&mut self.context,
0,
None,
&mut self.cursor,
&mut self.captures,
&mut self.highlights,
)?
.into();
let mut previous = std::mem::take(&mut self.injections);
for layer in &mut previous {
layer.dirty = edit.is_some_and(|edit| {
layer.ranges.iter().any(|range| {
edit.start_byte <= range.end_byte && edit.old_end_byte >= range.start_byte
})
});
if let Some(edit) = edit {
layer.tree.edit(edit);
layer.ranges = layer.tree.included_ranges();
layer.context = None;
}
}
while let Some(request) = queue.pop_front() {
// An identical language/range ancestor is a genuine injection cycle. Keep that host
// layer, but do not inject it into itself again (e.g. a grammar's injection.self).
if request.ancestors.iter().any(|(spec, ranges)| {
std::ptr::eq(*spec, request.syntax) && *ranges == request.ranges
}) {
continue;
}
if request.depth > 16 {
return Err("injection nesting exceeds 16 language layers".to_owned());
}
let configuration = configuration(request.syntax)?;
let previous_index = previous
.iter()
.position(|layer| {
std::ptr::eq(layer.syntax, request.syntax)
&& layer.depth == request.depth
&& layer.ranges == request.ranges
})
.or_else(|| {
previous.iter().position(|layer| {
std::ptr::eq(layer.syntax, request.syntax) && layer.depth == request.depth
})
});
let old = previous_index.map(|index| previous.swap_remove(index));
let unchanged = old
.as_ref()
.is_some_and(|layer| !layer.dirty && layer.ranges == request.ranges);
let (tree, mut context) = if unchanged {
let old = old.unwrap();
(old.tree, old.context)
} else {
self.injection_parser
.set_language(&request.syntax.language)
.map_err(|error| error.to_string())?;
self.injection_parser
.set_included_ranges(&request.ranges)
.map_err(|error| error.to_string())?;
#[cfg(test)]
{
self.injection_parses += 1;
}
let tree = self
.injection_parser
.parse(
self.source.as_bytes(),
old.as_ref().map(|layer| &layer.tree),
)
.ok_or_else(|| {
format!(
"{} injection parser stopped without a tree",
request.syntax.name
)
})?;
(tree, None)
};
// Markdown's inline grammar refines the block grammar, so plain inline text keeps
// its enclosing heading/link foreground. Other languages replace host strings/markup.
let refines_host = languages::by_name("markdown_inline")
.is_some_and(|inline| std::ptr::eq(inline, request.syntax));
if !refines_host {
for range in request
.ranges
.iter()
.filter(|range| intersects(visible, range.start_byte, range.end_byte))
{
self.highlights.push(Highlight {
start: range.start_byte,
end: range.end_byte,
kind: None,
depth: request.depth,
priority: i32::MIN,
pattern: 0,
});
}
}
let mut children = collect_layer(
configuration,
&tree,
&self.source,
&request.ranges,
visible,
&mut context,
request.depth,
Some(request.parent),
&mut self.cursor,
&mut self.captures,
&mut self.highlights,
)?;
let mut ancestors = request.ancestors;
ancestors.push((request.syntax, request.ranges.clone()));
for child in &mut children {
child.ancestors = ancestors.clone();
}
queue.extend(children);
self.injections.push(Layer {
syntax: request.syntax,
tree,
ranges: request.ranges,
depth: request.depth,
dirty: false,
context,
});
}
render_spans(
&self.source,
&self.line_starts,
&mut self.lines,
&mut self.string_spans,
&self.highlights,
&mut self.events,
visible,
);
Ok(())
}
}
fn syntax_prefix(text: &str) -> &str {
let mut end = text.len().min(MAX_SYNTAX_LINE_BYTES);
while !text.is_char_boundary(end) {
end -= 1;
}
text[..end].split('\n').next().unwrap_or_default()
}
fn syntax_source_length(lines: &[String], final_newline: bool) -> Result<usize, &'static str> {
let mut length = lines
.len()
.saturating_sub(1)
.saturating_add(usize::from(final_newline));
if length > MAX_SYNTAX_BYTES {
return Err(SOURCE_BUDGET_NOTICE);
}
for line in lines {
if line.len() > MAX_SYNTAX_LINE_BYTES {
return Err(LINE_BUDGET_NOTICE);
}
length = length.saturating_add(line.len());
if length > MAX_SYNTAX_BYTES {
return Err(SOURCE_BUDGET_NOTICE);
}
}
Ok(length)
}
fn has_scope_language(language: &str) -> bool {
matches!(
language,
"rust"
| "python"
| "ruby"
| "javascript"
| "typescript"
| "typescriptreact"
| "go"
| "c"
| "cpp"
| "shellscript"
| "zig"
)
}
fn collect_scopes(
root: Node<'_>,
language: &str,
source: &str,
ranges: &[Range],
scopes: &mut Vec<CachedScope>,
rows: &mut [Option<usize>],
) {
if !has_scope_language(language) {
return;
}
let mut cursor = root.walk();
let mut ancestors: Vec<(usize, usize)> = Vec::new();
loop {
let node = cursor.node();
while ancestors
.last()
.is_some_and(|(_, end)| *end <= node.start_byte())
{
ancestors.pop();
}
if let Some((extent, name, label)) =
scope_declaration(node, language, source).filter(|(extent, _, _)| {
ranges.is_empty()
|| ranges.iter().any(|range| {
range.start_byte <= extent.start_byte()
&& extent.end_byte() <= range.end_byte
})
})
{
let parent = ancestors
.last()
.map_or("", |(index, _)| scopes[*index].label.as_str());
let label = if parent.is_empty() {
label
} else {
format!("{parent} › {label}")
};
let start_row = extent.start_position().row;
let end = extent.end_position();
let end_row = end.row + usize::from(end.column > 0);
let index = scopes.len();
scopes.push(CachedScope {
start_row,
end_row,
name,
label,
});
let end_row = end_row.min(rows.len());
rows[start_row.min(end_row)..end_row].fill(Some(index));
ancestors.push((index, node.end_byte()));
}
if cursor.goto_first_child() {
continue;
}
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
return;
}
}
}
}
fn scope_text<'a>(node: Node<'_>, source: &'a str) -> Option<&'a str> {
(!node.has_error() && !node.is_missing() && node.kind() != "computed_property_name")
.then(|| source.get(node.byte_range()))
.flatten()
.filter(|name| !name.trim().is_empty())
}
fn scope_declaration<'a>(
node: Node<'a>,
language: &str,
source: &str,
) -> Option<(Node<'a>, Point, String)> {
if !node.is_named() || node.is_missing() {
return None;
}
let field = |name| node.child_by_field_name(name);
let mut extent = node;
let name = match (language, node.kind()) {
("rust", "impl_item") => {
let target = scope_text(field("type")?, source)?;
let label = match field("trait").and_then(|node| scope_text(node, source)) {
Some(trait_name) => scope_label(&["impl", trait_name, "for", target]),
None => scope_label(&["impl", target]),
};
return Some((node, field("type")?.start_position(), label));
}
(
"rust",
"function_item"
| "function_signature_item"
| "trait_item"
| "mod_item"
| "struct_item"
| "enum_item",
)
| ("python", "function_definition" | "class_definition")
| ("ruby", "class" | "module" | "method" | "singleton_method")
| (
"javascript" | "typescript" | "typescriptreact",
"function_declaration"
| "generator_function_declaration"
| "method_definition"
| "class_declaration"
| "class"
| "abstract_class_declaration"
| "interface_declaration"
| "internal_module",
)
| ("go", "function_declaration")
| (
"c" | "cpp",
"class_specifier"
| "struct_specifier"
| "union_specifier"
| "enum_specifier"
| "namespace_definition",
)
| ("shellscript", "function_definition")
| ("zig", "function_declaration") => field("name")?,
("go", "method_declaration") => {
let receiver = field("receiver")?
.named_child(0)?
.child_by_field_name("type")?;
let receiver = scope_text(receiver, source)?;
let name = field("name")?;
return Some((
node,
name.start_position(),
scope_label(&[receiver, "›", scope_text(name, source)?]),
));
}
("go", "type_spec")
if field("type")
.is_some_and(|node| matches!(node.kind(), "struct_type" | "interface_type")) =>
{
field("name")?
}
("c" | "cpp", "function_definition") => {
let mut declarator = field("declarator")?;
loop {
if matches!(
declarator.kind(),
"identifier"
| "field_identifier"
| "qualified_identifier"
| "operator_name"
| "destructor_name"
) {
break declarator;
}
declarator = declarator.child_by_field_name("declarator").or_else(|| {
matches!(
declarator.kind(),
"reference_declarator" | "parenthesized_declarator"
)
.then(|| declarator.named_child(0))
.flatten()
})?;
}
}
(
"javascript" | "typescript" | "typescriptreact",
"arrow_function" | "function_expression" | "generator_function",
) => {
if let Some(name) = field("name") {
name
} else {
extent = node.parent()?;
match extent.kind() {
"variable_declarator" => {
let name = extent.child_by_field_name("name")?;
(name.kind() == "identifier").then_some(name)?
}
"pair" => extent.child_by_field_name("key")?,
"field_definition" => extent.child_by_field_name("property")?,
"public_field_definition" => extent.child_by_field_name("name")?,
"assignment_expression" => {
let name = extent.child_by_field_name("left")?;
matches!(name.kind(), "identifier" | "member_expression").then_some(name)?
}
_ => return None,
}
}
}
(
"zig",
"struct_declaration" | "union_declaration" | "enum_declaration" | "opaque_declaration",
) => {
extent = node.parent()?;
if extent.kind() != "variable_declaration"
|| node.prev_sibling().is_none_or(|node| node.kind() != "=")
{
return None;
}
let mut cursor = extent.walk();
extent
.named_children(&mut cursor)
.find(|node| node.kind() == "identifier")?
}
_ => return None,
};
if let Some(parent) = node.parent()
&& parent.kind() == "decorated_definition"
{
extent = parent;
}
Some((
extent,
name.start_position(),
scope_label(&[scope_text(name, source)?]),
))
}
fn scope_label(parts: &[&str]) -> String {
let mut label = String::new();
for word in parts.iter().flat_map(|part| part.split_whitespace()) {
if !label.is_empty() {
label.push(' ');
}
label.extend(
word.chars()
.map(|ch| if ch.is_control() { '�' } else { ch }),
);
}
label
}
fn same_language(left: Option<&LanguageSpec>, right: Option<&LanguageSpec>) -> bool {
match (left, right) {
(Some(left), Some(right)) => std::ptr::eq(left, right),
(None, None) => true,
_ => false,
}
}
/// One aggregate edit, including byte columns (not character/display columns).
fn minimal_edit(old: &str, new: &str) -> InputEdit {
let mut start = old
.bytes()
.zip(new.bytes())
.take_while(|(a, b)| a == b)
.count();
while !old.is_char_boundary(start) || !new.is_char_boundary(start) {
start -= 1;
}
let mut suffix = old.as_bytes()[start..]
.iter()
.rev()
.zip(new.as_bytes()[start..].iter().rev())
.take_while(|(a, b)| a == b)
.count();
while !old.is_char_boundary(old.len() - suffix) || !new.is_char_boundary(new.len() - suffix) {
suffix -= 1;
}
let old_end = old.len() - suffix;
let new_end = new.len() - suffix;
InputEdit {
start_byte: start,
old_end_byte: old_end,
new_end_byte: new_end,
start_position: point_at(old, start),
old_end_position: point_at(old, old_end),
new_end_position: point_at(new, new_end),
}
}
fn point_at(source: &str, byte: usize) -> Point {
let prefix = &source.as_bytes()[..byte];
Point::new(
prefix.iter().filter(|byte| **byte == b'\n').count(),
prefix
.iter()
.rposition(|byte| *byte == b'\n')
.map_or(byte, |last| byte - last - 1),
)
}
struct Configuration {
syntax: &'static LanguageSpec,
query: Query,
context_query: Query,
highlights_query: Query,
locals_start: usize,
highlights_start: usize,
}
impl Configuration {
fn new(
syntax: &'static LanguageSpec,
query: Query,
locals_start: usize,
highlights_start: usize,
) -> Self {
let mut context_query = query.deep_clone();
let mut highlights_query = query.deep_clone();
for pattern in 0..query.pattern_count() {
if pattern < highlights_start {
highlights_query.disable_pattern(pattern);
} else {
context_query.disable_pattern(pattern);
}
}
Self {
syntax,
query,
context_query,
highlights_query,
locals_start,
highlights_start,
}
}
}
fn configuration(syntax: &'static LanguageSpec) -> Result<&'static Configuration, String> {
static CONFIGURATIONS: LazyLock<Vec<(&'static LanguageSpec, Result<Configuration, String>)>> =
LazyLock::new(|| {
languages::all()
.map(|syntax| (syntax, compile_configuration(syntax)))
.collect()
});
let (_, result) = CONFIGURATIONS
.iter()
.find(|(spec, _)| std::ptr::eq(*spec, syntax))
.ok_or_else(|| format!("missing {} query configuration", syntax.name))?;
result.as_ref().map_err(Clone::clone)
}
fn compile_configuration(syntax: &'static LanguageSpec) -> Result<Configuration, String> {
let source = format!(
"{}\n{}\n{}",
syntax.injections, syntax.locals, syntax.highlights
);
let locals_offset = syntax.injections.len() + 1;
let highlights_offset = locals_offset + syntax.locals.len() + 1;
let query = Query::new(&syntax.language, &source).map_err(|error| error.to_string())?;
let mut locals_start = 0;
let mut highlights_start = 0;
for pattern in 0..query.pattern_count() {
let start = query.start_byte_for_pattern(pattern);
locals_start += usize::from(start < locals_offset);
highlights_start += usize::from(start < highlights_offset);
if let Some(predicate) = query.general_predicates(pattern).first() {
return Err(format!(
"unsupported query predicate #{}",
predicate.operator
));
}
if let Some((property, _)) = query
.property_predicates(pattern)
.iter()
.find(|(property, _)| property.key.as_ref() != "local")
{
return Err(format!(
"unsupported query property predicate {}",
property.key
));
}
}
Ok(Configuration::new(
syntax,
query,
locals_start,
highlights_start,
))
}
fn classify(capture: &str, numeric_literal: bool) -> Option<SyntaxKind> {
let mut parts = capture.split('.');
match parts.next()? {
"comment" => Some(SyntaxKind::Comment),
"string" | "character" => Some(SyntaxKind::String),
"keyword" | "boolean" | "storage" | "conditional" | "repeat" | "include" | "exception" => {
Some(SyntaxKind::Keyword)
}
"number" | "float" => Some(SyntaxKind::Number),
"constant" if numeric_literal || parts.any(|part| part == "numeric") => {
Some(SyntaxKind::Number)
}
"constant" => Some(SyntaxKind::Keyword),
"type" | "constructor" => Some(SyntaxKind::Type),
"function" | "method" => Some(SyntaxKind::Function),
"markup" | "tag" | "text" => Some(SyntaxKind::Markup),
_ => None,
}
}
struct Layer {
syntax: &'static LanguageSpec,
tree: Tree,
ranges: Vec<Range>,
depth: usize,
dirty: bool,
context: Option<LayerContext>,
}
#[derive(Clone)]
struct Injection {
syntax: &'static LanguageSpec,
parent: &'static LanguageSpec,
ranges: Vec<Range>,
depth: usize,
ancestors: Vec<(&'static LanguageSpec, Vec<Range>)>,
}
struct LayerContext {
captures: Vec<Capture>,
injections: Vec<Injection>,
}
#[derive(Clone, Copy, Eq, PartialEq)]
struct Capture {
node: usize,
start: usize,
end: usize,
capture: u32,
pattern: usize,
value_end: usize,
numeric_literal: bool,
}
impl Capture {
fn new(
capture: &tree_sitter::QueryCapture<'_>,
pattern: usize,
value_end: usize,
name: &str,
) -> Self {
Self {
node: capture.node.id(),
start: capture.node.start_byte(),
end: capture.node.end_byte(),
capture: capture.index,
pattern,
value_end,
numeric_literal: name.starts_with("constant")
&& matches!(
capture.node.kind(),
"integer_literal"
| "float_literal"
| "number_literal"
| "integer"
| "float"
| "number"
),
}
}
}
struct Highlight {
start: usize,
end: usize,
kind: Option<SyntaxKind>,
depth: usize,
priority: i32,
pattern: usize,
}
struct Definition {
value_end: usize,
highlight: Option<SyntaxKind>,
}
struct Scope<'a> {
end: usize,
inherits: bool,
definitions: HashMap<&'a str, Vec<Definition>>,
}
#[allow(clippy::too_many_arguments)]
fn collect_layer(
configuration: &Configuration,
tree: &Tree,
source: &str,
ranges: &[Range],
visible: &[Range],
context: &mut Option<LayerContext>,
depth: usize,
parent: Option<&'static LanguageSpec>,
cursor: &mut QueryCursor,
captures: &mut Vec<Capture>,
highlights: &mut Vec<Highlight>,
) -> Result<Vec<Injection>, String> {
let query = &configuration.query;
let names = query.capture_names();
captures.clear();
let mut injections;
if let Some(context) = context {
captures.extend_from_slice(&context.captures);
injections = context.injections.clone();
} else {
injections = Vec::new();
let mut combined: HashMap<(usize, usize), usize> = HashMap::new();
cursor.set_byte_range(0..source.len());
let mut matches = cursor.matches(
&configuration.context_query,
tree.root_node(),
source.as_bytes(),
);
while let Some(found) = matches.next() {
let pattern = found.pattern_index;
if pattern < configuration.locals_start {
let mut language = None;
let mut nodes = Vec::new();
for capture in found.captures() {
match names[capture.index as usize] {
"injection.language" => {
language = source[capture.node.byte_range()]
.split_whitespace()
.next()
.and_then(languages::by_name);
}
"injection.content" => nodes.push(capture.node),
_ => {}
}
}
let mut include_children = false;
let mut is_combined = false;
for property in query.property_settings(pattern) {
match property.key.as_ref() {
"injection.language" => {
language = property.value.as_deref().and_then(languages::by_name);
}
"injection.self" => language = Some(configuration.syntax),
"injection.parent" => language = parent,
"injection.include-children" => include_children = true,
"injection.combined" => is_combined = true,
_ => {}
}
}
if let Some(language) = language {
let mut content = injection_ranges(&nodes, ranges, include_children);
if !content.is_empty() {
let key = (pattern, language as *const LanguageSpec as usize);
if is_combined && let Some(index) = combined.get(&key) {
let injection: &mut Injection = &mut injections[*index];
injection.ranges.append(&mut content);
} else {
if is_combined {
combined.insert(key, injections.len());
}
injections.push(Injection {
syntax: language,
parent: configuration.syntax,
ranges: content,
depth: depth + 1,
ancestors: vec![(configuration.syntax, ranges.to_vec())],
});
}
}
}
continue;
}
let value_end = found
.captures()
.iter()
.find(|capture| names[capture.index as usize] == "local.definition-value")
.map_or(0, |capture| capture.node.end_byte());
for capture in found.captures() {
let name = names[capture.index as usize];
if name.starts_with('_') || name == "local.definition-value" {
continue;
}
captures.push(Capture::new(capture, pattern, value_end, name));
}
}
if cursor.did_exceed_match_limit() {
return Err(format!(
"{} query exceeded its match limit",
configuration.syntax.name
));
}
for injection in &mut injections {
normalize_ranges(&mut injection.ranges);
}
// Locals must be resolved in source order, including definitions above the viewport.
// Query only their nodes for the foreground that references inherit.
let definitions: BTreeSet<_> = captures
.iter()
.filter(|capture| names[capture.capture as usize] == "local.definition")
.map(|capture| capture.node)
.collect();
let mut definition_ranges: Vec<_> = captures
.iter()
.filter(|capture| definitions.contains(&capture.node))
.map(|capture| Range {
start_byte: capture.start,
end_byte: capture.end,
start_point: Point::new(0, 0),
end_point: Point::new(0, 0),
})
.collect();
normalize_ranges(&mut definition_ranges);
collect_highlight_captures(
configuration,
tree,
source,
&definition_ranges,
Some(&definitions),
cursor,
captures,
)?;
*context = Some(LayerContext {
captures: captures.clone(),
injections: injections.clone(),
});
}
collect_highlight_captures(configuration, tree, source, visible, None, cursor, captures)?;
captures.sort_unstable_by_key(|capture| {
(
capture.start,
Reverse(capture.end),
capture.node,
capture.pattern,
capture.capture,
)
});
captures.dedup();
let mut scopes = vec![Scope {
end: usize::MAX,
inherits: false,
definitions: HashMap::new(),
}];
let mut start = 0;
while start < captures.len() {
let first = captures[start];
let mut end = start + 1;
while end < captures.len() && captures[end].node == first.node {
end += 1;
}
let group = &captures[start..end];
while scopes.len() > 1 && first.start >= scopes.last().unwrap().end {
scopes.pop();
}
let mut definition = None;
let mut reference = None;
let mut is_local = false;
for capture in group
.iter()
.filter(|capture| capture.pattern < configuration.highlights_start)
{
match names[capture.capture as usize] {
"local.scope" => {
let inherits = query
.property_settings(capture.pattern)
.iter()
.find(|property| property.key.as_ref() == "local.scope-inherits")
.is_none_or(|property| {
property
.value
.as_deref()
.is_none_or(|value| value == "true")
});
scopes.push(Scope {
end: capture.end,
inherits,
definitions: HashMap::new(),
});
}
"local.definition" => {
is_local = true;
definition = Some(capture.value_end);
}
"local.reference" if definition.is_none() => {
let name = &source[capture.start..capture.end];
for scope in scopes.iter().rev() {
if let Some(found) = scope.definitions.get(name).and_then(|definitions| {
definitions
.iter()
.rev()
.find(|definition| capture.start >= definition.value_end)
}) {
is_local = true;
reference = Some(found.highlight);
break;
}
if !scope.inherits {
break;
}
}
}
_ => {}
}
}
let selected = group
.iter()
.filter(|capture| {
capture.pattern >= configuration.highlights_start
&& query.property_predicates(capture.pattern).iter().all(
|(property, positive)| {
let local = property
.capture_id
.map_or(is_local, |id| id == capture.capture as usize && is_local);
local == *positive
},
)
})
.max_by_key(|capture| {
(
capture_priority(query, capture),
capture.pattern,
capture.capture,
)
});
let kind = reference.unwrap_or_else(|| {
selected.and_then(|capture| {
classify(names[capture.capture as usize], capture.numeric_literal)
})
});
if let Some(value_end) = definition {
let name = &source[first.start..first.end];
scopes
.last_mut()
.unwrap()
.definitions
.entry(name)
.or_default()
.push(Definition {
value_end,
highlight: kind,
});
}
if let Some(kind) = kind {
let selected = selected.copied().unwrap_or(first);
let range_start = ranges.partition_point(|range| range.end_byte <= first.start);
for range in ranges[range_start..]
.iter()
.take_while(|range| range.start_byte < first.end)
{
let low = first.start.max(range.start_byte);
let high = first.end.min(range.end_byte);
if low < high && intersects(visible, low, high) {
highlights.push(Highlight {
start: low,
end: high,
kind: Some(kind),
depth,
priority: capture_priority(query, &selected),
pattern: selected.pattern,
});
}
}
}
start = end;
}
Ok(injections)
}
fn intersects(ranges: &[Range], start: usize, end: usize) -> bool {
ranges
.get(ranges.partition_point(|range| range.end_byte <= start))
.is_some_and(|range| range.start_byte < end)
}
#[allow(clippy::too_many_arguments)]
fn collect_highlight_captures(
configuration: &Configuration,
tree: &Tree,
source: &str,
ranges: &[Range],
definitions: Option<&BTreeSet<usize>>,
cursor: &mut QueryCursor,
captures: &mut Vec<Capture>,
) -> Result<(), String> {
let names = configuration.query.capture_names();
for range in ranges {
if range.start_byte == range.end_byte {
continue;
}
cursor.set_byte_range(range.start_byte..range.end_byte);
let mut matches = cursor.matches(
&configuration.highlights_query,
tree.root_node(),
source.as_bytes(),
);
while let Some(found) = matches.next() {
for capture in found.captures() {
let name = names[capture.index as usize];
if name.starts_with('_')
|| capture.node.end_byte() <= range.start_byte
|| capture.node.start_byte() >= range.end_byte
|| definitions
.is_some_and(|definitions| !definitions.contains(&capture.node.id()))
{
continue;
}
captures.push(Capture::new(capture, found.pattern_index, 0, name));
}
}
if cursor.did_exceed_match_limit() {
return Err(format!(
"{} query exceeded its match limit",
configuration.syntax.name
));
}
}
Ok(())
}
fn capture_priority(query: &Query, capture: &Capture) -> i32 {
query
.property_settings(capture.pattern)
.iter()
.rev()
.find_map(|property| {
if property.key.as_ref() == "priority"
&& property
.capture_id
.is_none_or(|id| id == capture.capture as usize)
{
property.value.as_deref()?.parse().ok()
} else {
None
}
})
.unwrap_or(0)
}
fn injection_ranges(nodes: &[Node<'_>], parents: &[Range], include_children: bool) -> Vec<Range> {
let mut result = Vec::new();
for node in nodes {
let mut start_byte = node.start_byte();
let mut start_point = node.start_position();
let mut cursor = node.walk();
let excluded: Vec<_> = if include_children {
Vec::new()
} else {
// The bundled Markdown queries are Neovim-derived: anonymous punctuation is
// injected source, while named children (e.g. block continuations) stay in the host.
node.named_children(&mut cursor)
.map(|child| child.range())
.collect()
};
for excluded in excluded.into_iter().chain(std::iter::once(Range {
start_byte: node.end_byte(),
end_byte: node.end_byte(),
start_point: node.end_position(),
end_point: node.end_position(),
})) {
let parent_start = parents.partition_point(|range| range.end_byte <= start_byte);
for parent in parents[parent_start..]
.iter()
.take_while(|range| range.start_byte < excluded.start_byte)
{
let low = start_byte.max(parent.start_byte);
let high = excluded.start_byte.min(parent.end_byte);
if low < high {
result.push(Range {
start_byte: low,
end_byte: high,
start_point: if low == start_byte {
start_point
} else {
parent.start_point
},
end_point: if high == excluded.start_byte {
excluded.start_point
} else {
parent.end_point
},
});
}
}
start_byte = excluded.end_byte;
start_point = excluded.end_point;
}
}
normalize_ranges(&mut result);
result
}
fn normalize_ranges(ranges: &mut Vec<Range>) {
ranges.sort_unstable_by_key(|range| (range.start_byte, range.end_byte));
let mut length = 0;
for index in 0..ranges.len() {
let range = ranges[index];
if length > 0 && ranges[length - 1].end_byte >= range.start_byte {
let previous = &mut ranges[length - 1];
if previous.end_byte < range.end_byte {
previous.end_byte = range.end_byte;
previous.end_point = range.end_point;
}
} else {
ranges[length] = range;
length += 1;
}
}
ranges.truncate(length);
}
fn render_spans(
source: &str,
starts: &[usize],
lines: &mut [Vec<SyntaxSpan>],
strings: &mut [Vec<SyntaxSpan>],
highlights: &[Highlight],
events: &mut Vec<(usize, bool, usize)>,
visible: &[Range],
) {
events.clear();
for (index, highlight) in highlights.iter().enumerate() {
events.push((highlight.start, true, index));
events.push((highlight.end, false, index));
}
events.sort_unstable();
let rank = |index: usize| {
let highlight = &highlights[index];
(
highlight.depth,
highlight.priority,
Reverse(highlight.end - highlight.start),
highlight.pattern,
index,
)
};
let mut active: BTreeSet<(_, usize)> = BTreeSet::new();
let mut previous = 0;
let mut active_strings = 0;
for &(byte, entering, index) in events.iter() {
if previous < byte {
let kind = active
.last()
.and_then(|(_, current)| highlights[*current].kind);
if kind.is_some() || active_strings > 0 {
let start = visible.partition_point(|range| range.end_byte <= previous);
for range in visible[start..]
.iter()
.take_while(|range| range.start_byte < byte)
{
let low = previous.max(range.start_byte);
let high = byte.min(range.end_byte);
if let Some(kind) = kind {
append_global(source, starts, lines, low, high, kind, false);
}
if active_strings > 0 {
append_global(source, starts, strings, low, high, SyntaxKind::String, true);
}
}
}
}
if highlights[index].kind == Some(SyntaxKind::String) {
if entering {
active_strings += 1;
} else {
active_strings -= 1;
}
}
if entering {
active.insert((rank(index), index));
} else {
active.remove(&(rank(index), index));
}
previous = byte;
}
}
fn append_global(
source: &str,
starts: &[usize],
lines: &mut [Vec<SyntaxSpan>],
mut start: usize,
end: usize,
kind: SyntaxKind,
include_newline: bool,
) {
if starts.is_empty() {
return;
}
let mut row = starts
.partition_point(|offset| *offset <= start)
.saturating_sub(1);
while start < end && row < lines.len() {
let line_end = starts.get(row + 1).map_or(source.len(), |next| next - 1);
let high = end.min(line_end + usize::from(include_newline && row + 1 < starts.len()));
if start < high {
let spans = &mut lines[row];
let low = start - starts[row];
let high = high - starts[row];
if let Some(previous) = spans.last_mut()
&& previous.end_byte == low
&& previous.kind == kind
{
previous.end_byte = high;
} else {
spans.push(SyntaxSpan {
start_byte: low,
end_byte: high,
kind,
});
}
}
row += 1;
start = line_end + 1;
}
}
#[cfg(test)]
#[path = "../tests/unit/syntax.rs"]
mod tests;