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.
This commit is contained in:
+109
-6
@@ -14,6 +14,12 @@ 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")
|
||||
}
|
||||
@@ -69,6 +75,7 @@ pub struct SyntaxCache {
|
||||
path: Option<PathBuf>,
|
||||
final_newline: bool,
|
||||
syntax: Option<&'static LanguageSpec>,
|
||||
budget_exceeded: Option<&'static str>,
|
||||
source: String,
|
||||
next_source: String,
|
||||
line_starts: Vec<usize>,
|
||||
@@ -100,7 +107,34 @@ impl SyntaxCache {
|
||||
}
|
||||
|
||||
pub(crate) fn update_text(&mut self, text: &str, path: &Path) {
|
||||
if self.source == text
|
||||
// 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)
|
||||
{
|
||||
@@ -168,8 +202,11 @@ impl SyntaxCache {
|
||||
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(String::as_str))
|
||||
languages::detect(path, lines.first().map(|line| syntax_prefix(line)))
|
||||
} else {
|
||||
self.syntax
|
||||
};
|
||||
@@ -181,12 +218,29 @@ impl SyntaxCache {
|
||||
}
|
||||
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();
|
||||
let length = lines.iter().map(String::len).sum::<usize>()
|
||||
+ lines.len().saturating_sub(1)
|
||||
+ usize::from(final_newline);
|
||||
self.next_source.reserve(length);
|
||||
for (row, line) in lines.iter().enumerate() {
|
||||
if row > 0 {
|
||||
@@ -289,6 +343,27 @@ impl SyntaxCache {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -352,7 +427,7 @@ impl SyntaxCache {
|
||||
self.syntax.map(|syntax| syntax.name)
|
||||
}
|
||||
|
||||
/// Genuine grammar/query failures disable highlighting; ERROR nodes in user text do not.
|
||||
/// Why highlighting is unavailable: a deliberate plain-text fallback or grammar/query failure.
|
||||
pub fn error(&self) -> Option<&str> {
|
||||
self.error.as_deref()
|
||||
}
|
||||
@@ -546,6 +621,34 @@ impl SyntaxCache {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user