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:
+167
-23
@@ -295,6 +295,40 @@ struct Snapshot {
|
||||
primary: usize,
|
||||
}
|
||||
|
||||
/// Retain only changed rows between undo groups, not another copy of the file.
|
||||
#[cfg_attr(test, derive(Debug))]
|
||||
struct HistoryEntry {
|
||||
start: usize,
|
||||
end: usize,
|
||||
before: Snapshot,
|
||||
}
|
||||
|
||||
impl HistoryEntry {
|
||||
fn new(mut before: Snapshot, after: &[String]) -> Self {
|
||||
let start = before
|
||||
.lines
|
||||
.iter()
|
||||
.zip(after)
|
||||
.take_while(|(a, b)| a == b)
|
||||
.count();
|
||||
let suffix = before.lines[start..]
|
||||
.iter()
|
||||
.rev()
|
||||
.zip(after[start..].iter().rev())
|
||||
.take_while(|(a, b)| a == b)
|
||||
.count();
|
||||
before.lines = before
|
||||
.lines
|
||||
.drain(start..before.lines.len() - suffix)
|
||||
.collect();
|
||||
Self {
|
||||
start,
|
||||
end: after.len() - suffix,
|
||||
before,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Edit<'a> {
|
||||
target: Option<usize>,
|
||||
start: usize,
|
||||
@@ -1868,6 +1902,8 @@ struct Buffer {
|
||||
snapshot_root: Option<PathBuf>,
|
||||
lines: Vec<String>,
|
||||
saved_lines: Vec<String>,
|
||||
// Invalidate whenever text or the saved baseline changes, including temporary swaps.
|
||||
dirty_cache: Cell<Option<bool>>,
|
||||
disk_stamp: Option<FileStamp>,
|
||||
reload_error: Option<String>,
|
||||
text_revision: u64,
|
||||
@@ -1878,9 +1914,8 @@ struct Buffer {
|
||||
view: ViewState,
|
||||
path: Option<PathBuf>,
|
||||
identity: Option<PathBuf>,
|
||||
// ponytail: full-buffer history costs O(buffer size) per action; use edit deltas for large files.
|
||||
undo: Vec<Snapshot>,
|
||||
redo: Vec<Snapshot>,
|
||||
undo: Vec<HistoryEntry>,
|
||||
redo: Vec<HistoryEntry>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
@@ -1926,6 +1961,7 @@ impl Buffer {
|
||||
snapshot_document_path: None,
|
||||
snapshot_root: None,
|
||||
saved_lines: lines.clone(),
|
||||
dirty_cache: Cell::new(Some(false)),
|
||||
disk_stamp,
|
||||
reload_error: None,
|
||||
lines,
|
||||
@@ -1946,7 +1982,15 @@ impl Buffer {
|
||||
}
|
||||
|
||||
fn dirty(&self) -> bool {
|
||||
!self.resolver_readonly && self.diff_snapshot.is_none() && self.lines != self.saved_lines
|
||||
if self.resolver_readonly || self.diff_snapshot.is_some() {
|
||||
return false;
|
||||
}
|
||||
if let Some(dirty) = self.dirty_cache.get() {
|
||||
return dirty;
|
||||
}
|
||||
let dirty = self.lines != self.saved_lines;
|
||||
self.dirty_cache.set(Some(dirty));
|
||||
dirty
|
||||
}
|
||||
|
||||
fn refresh_file(&mut self) -> io::Result<bool> {
|
||||
@@ -1994,6 +2038,7 @@ impl Buffer {
|
||||
}
|
||||
self.replace_lines(split_lines(&text));
|
||||
self.saved_lines.clone_from(&self.lines);
|
||||
self.dirty_cache.set(Some(false));
|
||||
self.undo.clear();
|
||||
self.redo.clear();
|
||||
self.view.reload(&self.lines);
|
||||
@@ -2029,12 +2074,17 @@ impl Buffer {
|
||||
if let Some(tokens) = &mut self.semantic_tokens {
|
||||
retain_semantic_tokens(tokens, &self.lines, &lines);
|
||||
}
|
||||
self.text_revision = self.text_revision.wrapping_add(1);
|
||||
self.diagnostics.clear();
|
||||
self.view.search_match = None;
|
||||
self.text_changed();
|
||||
}
|
||||
std::mem::replace(&mut self.lines, lines)
|
||||
}
|
||||
|
||||
fn text_changed(&mut self) {
|
||||
self.text_revision = self.text_revision.wrapping_add(1);
|
||||
self.dirty_cache.set(None);
|
||||
self.diagnostics.clear();
|
||||
self.view.search_match = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -2601,15 +2651,18 @@ impl Editor {
|
||||
let seed = split_lines(projection.seed());
|
||||
if buffer.lines != seed {
|
||||
let previous = buffer.replace_lines(seed);
|
||||
buffer.undo.push(Snapshot {
|
||||
lines: previous,
|
||||
targets: if buffer.view.targets.is_empty() {
|
||||
vec![Target::caret(Cursor::default())]
|
||||
} else {
|
||||
buffer.view.targets.clone()
|
||||
buffer.undo.push(HistoryEntry::new(
|
||||
Snapshot {
|
||||
lines: previous,
|
||||
targets: if buffer.view.targets.is_empty() {
|
||||
vec![Target::caret(Cursor::default())]
|
||||
} else {
|
||||
buffer.view.targets.clone()
|
||||
},
|
||||
primary: buffer.view.primary,
|
||||
},
|
||||
primary: buffer.view.primary,
|
||||
});
|
||||
&buffer.lines,
|
||||
));
|
||||
buffer.redo.clear();
|
||||
buffer.view.clamp(&buffer.lines);
|
||||
for pane in self.panes.values_mut() {
|
||||
@@ -2675,12 +2728,14 @@ impl Editor {
|
||||
buffer.view.clamp(&buffer.lines);
|
||||
}
|
||||
buffer.saved_lines = saved;
|
||||
buffer.dirty_cache.set(None);
|
||||
buffer.identity = Some(target_path.clone());
|
||||
buffer.path = Some(target_path);
|
||||
buffer.resolver_readonly = false;
|
||||
} else if !buffer.dirty() {
|
||||
buffer.replace_lines(vec!["Non-text resolution saved; reopen the file to inspect its current type.".into()]);
|
||||
buffer.saved_lines.clone_from(&buffer.lines);
|
||||
buffer.dirty_cache.set(Some(false));
|
||||
buffer.path = None;
|
||||
buffer.identity = None;
|
||||
buffer.resolver_readonly = true;
|
||||
@@ -2983,6 +3038,7 @@ impl Editor {
|
||||
if buffer.lines != alternative {
|
||||
buffer.replace_lines(alternative.clone());
|
||||
buffer.saved_lines = alternative;
|
||||
buffer.dirty_cache.set(Some(false));
|
||||
buffer.view.clamp(&buffer.lines);
|
||||
for pane in self.panes.values_mut() {
|
||||
if let Some(view) = pane.views.get_mut(&alternative_buffer) {
|
||||
@@ -5024,6 +5080,7 @@ impl Editor {
|
||||
buffer.snapshot_document_path = Some(document_path);
|
||||
buffer.snapshot_root = Some(root);
|
||||
buffer.lines = lines;
|
||||
buffer.dirty_cache.set(None);
|
||||
buffer
|
||||
}
|
||||
|
||||
@@ -10028,6 +10085,7 @@ impl Editor {
|
||||
match atomic_save(&path, &self.active.lines, expected) {
|
||||
Ok(stamp) => {
|
||||
self.active.saved_lines.clone_from(&self.active.lines);
|
||||
self.active.dirty_cache.set(Some(false));
|
||||
self.active.save_revision = self.active.save_revision.wrapping_add(1);
|
||||
self.active.disk_stamp = Some(stamp);
|
||||
self.active.reload_error = None;
|
||||
@@ -10063,7 +10121,9 @@ impl Editor {
|
||||
fn record(&mut self, before: Snapshot) {
|
||||
if before.lines != self.active.lines {
|
||||
self.active.view.search_match = None;
|
||||
self.active.undo.push(before);
|
||||
self.active
|
||||
.undo
|
||||
.push(HistoryEntry::new(before, &self.active.lines));
|
||||
self.active.redo.clear();
|
||||
}
|
||||
}
|
||||
@@ -11369,6 +11429,7 @@ impl Editor {
|
||||
let linewise = self.active.view.linewise;
|
||||
let search_match = self.active.view.search_match;
|
||||
let revision = self.active.text_revision;
|
||||
let dirty_cache = self.active.dirty_cache.get();
|
||||
let diagnostics = std::mem::take(&mut self.active.diagnostics);
|
||||
let semantic_tokens = self.active.semantic_tokens.clone();
|
||||
let mut restored = false;
|
||||
@@ -11380,6 +11441,7 @@ impl Editor {
|
||||
// Validate the entire replay first, then publish while the original
|
||||
// document is restored, and only then commit the computed deletion.
|
||||
std::mem::swap(&mut self.active.lines, &mut before.lines);
|
||||
self.active.dirty_cache.set(None);
|
||||
std::mem::swap(&mut self.active.view.targets, &mut before.targets);
|
||||
std::mem::swap(&mut self.active.view.primary, &mut before.primary);
|
||||
restored = true;
|
||||
@@ -11389,6 +11451,7 @@ impl Editor {
|
||||
shape,
|
||||
)?;
|
||||
std::mem::swap(&mut self.active.lines, &mut before.lines);
|
||||
self.active.dirty_cache.set(None);
|
||||
std::mem::swap(&mut self.active.view.targets, &mut before.targets);
|
||||
std::mem::swap(&mut self.active.view.primary, &mut before.primary);
|
||||
restored = false;
|
||||
@@ -11412,6 +11475,7 @@ impl Editor {
|
||||
self.active.view.primary = before.primary;
|
||||
}
|
||||
self.active.text_revision = revision;
|
||||
self.active.dirty_cache.set(dirty_cache);
|
||||
self.active.diagnostics = diagnostics;
|
||||
self.active.semantic_tokens = semantic_tokens;
|
||||
self.active.view.mode = mode;
|
||||
@@ -11677,13 +11741,35 @@ impl Editor {
|
||||
self.normalize_targets();
|
||||
}
|
||||
|
||||
fn restore_history(&mut self, entry: HistoryEntry) -> HistoryEntry {
|
||||
let mut lines = self.active.lines.clone();
|
||||
let end = entry.start + entry.before.lines.len();
|
||||
let before = Snapshot {
|
||||
lines: lines
|
||||
.splice(entry.start..entry.end, entry.before.lines)
|
||||
.collect(),
|
||||
targets: self.active.view.targets.clone(),
|
||||
primary: self.active.view.primary,
|
||||
};
|
||||
self.restore(Snapshot {
|
||||
lines,
|
||||
targets: entry.before.targets,
|
||||
primary: entry.before.primary,
|
||||
});
|
||||
HistoryEntry {
|
||||
start: entry.start,
|
||||
end,
|
||||
before,
|
||||
}
|
||||
}
|
||||
|
||||
fn undo(&mut self) {
|
||||
if self.mutation_blocked() {
|
||||
return;
|
||||
}
|
||||
if let Some(before) = self.active.undo.pop() {
|
||||
self.active.redo.push(self.snapshot());
|
||||
self.restore(before);
|
||||
let reverse = self.restore_history(before);
|
||||
self.active.redo.push(reverse);
|
||||
} else {
|
||||
self.set_message("Nothing to undo");
|
||||
}
|
||||
@@ -11694,8 +11780,8 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
if let Some(after) = self.active.redo.pop() {
|
||||
self.active.undo.push(self.snapshot());
|
||||
self.restore(after);
|
||||
let reverse = self.restore_history(after);
|
||||
self.active.undo.push(reverse);
|
||||
} else {
|
||||
self.set_message("Nothing to redo");
|
||||
}
|
||||
@@ -11951,6 +12037,61 @@ impl Editor {
|
||||
if edits.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Ordinary typing/deletion can edit one line without copying the entire file.
|
||||
// Snapshot previews, shared panes and semantic overlays use the general remapping path.
|
||||
if original.is_none()
|
||||
&& edits.len() == 1
|
||||
&& self.panes.len() == 1
|
||||
&& self.active.semantic_tokens.is_none()
|
||||
&& !edits[0].text.contains('\n')
|
||||
{
|
||||
let edit = &edits[0];
|
||||
let mut offsets = line_offsets(&self.active.lines);
|
||||
let row = offsets
|
||||
.partition_point(|offset| *offset <= edit.start)
|
||||
.saturating_sub(1);
|
||||
let start = edit.start - offsets[row];
|
||||
let end = edit.end - offsets[row];
|
||||
if end <= self.active.lines[row].len() {
|
||||
let mut destinations: Vec<_> = self
|
||||
.active
|
||||
.view
|
||||
.targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
(
|
||||
translate_edit_offset(
|
||||
cursor_offset(&self.active.lines, &offsets, target.active),
|
||||
true,
|
||||
std::iter::once(edit),
|
||||
),
|
||||
true,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
if let Some(target) = edit.target {
|
||||
destinations[target] = (edit.start + edit.caret, edit.round_up);
|
||||
}
|
||||
if self.active.lines[row][start..end] != *edit.text {
|
||||
self.active.lines[row].replace_range(start..end, &edit.text);
|
||||
self.active.text_changed();
|
||||
}
|
||||
for offset in &mut offsets[row + 1..] {
|
||||
*offset = *offset - (end - start) + edit.text.len();
|
||||
}
|
||||
for (target, (byte, round_up)) in
|
||||
self.active.view.targets.iter_mut().zip(destinations)
|
||||
{
|
||||
target.active = offset_cursor(&self.active.lines, &offsets, byte, round_up);
|
||||
target.anchor = target.active;
|
||||
target.preferred_columns = None;
|
||||
}
|
||||
if !preserve_targets {
|
||||
self.normalize_targets();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
edits.sort_by_key(|edit| (edit.start, edit.end));
|
||||
let mut unique: Vec<usize> = Vec::with_capacity(edits.len());
|
||||
for (index, edit) in edits.iter().enumerate() {
|
||||
@@ -13115,7 +13256,7 @@ fn atomic_save(
|
||||
.parent()
|
||||
.filter(|parent| !parent.as_os_str().is_empty())
|
||||
.unwrap_or(Path::new("."));
|
||||
let (temporary, mut file) = loop {
|
||||
let (temporary, file) = loop {
|
||||
let id = TEMP_ID.fetch_add(1, Ordering::Relaxed);
|
||||
let temporary = parent.join(format!(".vamp-{}-{id}.tmp", std::process::id()));
|
||||
let mut options = OpenOptions::new();
|
||||
@@ -13129,12 +13270,15 @@ fn atomic_save(
|
||||
}
|
||||
};
|
||||
let result = (|| {
|
||||
let mut writer = io::BufWriter::new(&file);
|
||||
for (index, line) in lines.iter().enumerate() {
|
||||
if index > 0 {
|
||||
file.write_all(b"\n")?;
|
||||
writer.write_all(b"\n")?;
|
||||
}
|
||||
file.write_all(line.as_bytes())?;
|
||||
writer.write_all(line.as_bytes())?;
|
||||
}
|
||||
writer.flush()?;
|
||||
drop(writer);
|
||||
if let Some(metadata) = metadata {
|
||||
file.set_permissions(metadata.permissions())?;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user