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:
2026-10-16 21:14:16 +02:00
parent 02d1422a04
commit adbd2c3f81
13 changed files with 639 additions and 55 deletions
+122 -13
View File
@@ -323,6 +323,7 @@ fn resolver_explorer_retains_last_jj_conflict_state_until_confirmation() {
ResolverStatus::Unconfirmed
);
editor.active.saved_lines.clone_from(&editor.active.lines);
editor.active.dirty_cache.set(None);
editor.refresh_resolver_status();
assert_eq!(
editor.resolver().unwrap().files[0].status,
@@ -3245,7 +3246,10 @@ fn review_identity_changes_retire_drafts_before_reusing_comment_ids() {
let restored = root.join("restored.json");
editor.execute(&format!("diff.review.save {}", restored.display()));
assert_eq!(
crate::review::Review::load(&restored, &root).unwrap().comments[0].body,
crate::review::Review::load(&restored, &root)
.unwrap()
.comments[0]
.body,
"Original feedback"
);
@@ -7145,19 +7149,25 @@ mod system_clipboard {
editor
.apply(vec![Effect::Replace("aa\nbb\ncc".into())])
.unwrap();
editor.active.saved_lines.clone_from(&editor.active.lines);
editor.active.dirty_cache.set(Some(false));
assert!(!editor.dirty());
type_text(&mut editor, "Vj");
fixture.fail("fail-write", true);
let before = state(&editor);
editor.handle(Input::Char('.'));
assert_eq!(state(&editor), before);
assert!(!editor.dirty());
assert_eq!(fixture.bytes("state"), b"a\n");
assert!(editor.visual_linewise());
fixture.fail("fail-write", false);
editor.handle(Input::Char('.'));
assert_eq!(editor.context().text, "cc");
assert_eq!(fixture.bytes("state"), b"aa\nbb\n");
assert!(editor.dirty());
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "aa\nbb\ncc");
assert!(!editor.dirty());
assert_eq!(editor.mode(), Mode::Normal);
}
@@ -7795,11 +7805,12 @@ fn filter_escape_resume_and_clear_do_not_edit_underlying_buffers_or_history() {
editor.count = Some(7);
let snapshot =
|state: &Snapshot| (state.lines.clone(), state.targets.clone(), state.primary);
let history = |entry: &HistoryEntry| (entry.start, entry.end, snapshot(&entry.before));
let lines = editor.lines().to_owned();
let targets = editor.active.view.targets.clone();
let primary = editor.active.view.primary;
let undo: Vec<_> = editor.active.undo.iter().map(&snapshot).collect();
let redo: Vec<_> = editor.active.redo.iter().map(&snapshot).collect();
let undo: Vec<_> = editor.active.undo.iter().map(&history).collect();
let redo: Vec<_> = editor.active.redo.iter().map(&history).collect();
let insert = editor.active.view.insert_start.as_ref().map(snapshot);
let buffer = editor.buffer_id();
let buffers = editor.buffer_count();
@@ -7845,11 +7856,11 @@ fn filter_escape_resume_and_clear_do_not_edit_underlying_buffers_or_history() {
assert_eq!(editor.buffer_id(), buffer);
assert_eq!(editor.buffer_count(), buffers);
assert_eq!(
editor.active.undo.iter().map(&snapshot).collect::<Vec<_>>(),
editor.active.undo.iter().map(&history).collect::<Vec<_>>(),
undo
);
assert_eq!(
editor.active.redo.iter().map(&snapshot).collect::<Vec<_>>(),
editor.active.redo.iter().map(&history).collect::<Vec<_>>(),
redo
);
assert_eq!(
@@ -8078,11 +8089,12 @@ fn selection_navigation_wraps_without_touching_any_underlying_mode() {
assert!(len > 1);
let snapshot =
|state: &Snapshot| (state.lines.clone(), state.targets.clone(), state.primary);
let history = |entry: &HistoryEntry| (entry.start, entry.end, snapshot(&entry.before));
let text = editor.context().text;
let targets = editor.active.view.targets.clone();
let primary = editor.active.view.primary;
let undo: Vec<_> = editor.active.undo.iter().map(&snapshot).collect();
let redo: Vec<_> = editor.active.redo.iter().map(&snapshot).collect();
let undo: Vec<_> = editor.active.undo.iter().map(&history).collect();
let redo: Vec<_> = editor.active.redo.iter().map(&history).collect();
let insert = editor.active.view.insert_start.as_ref().map(snapshot);
editor.count = Some(7);
for (input, selected) in [
@@ -8104,11 +8116,11 @@ fn selection_navigation_wraps_without_touching_any_underlying_mode() {
assert_eq!(editor.active.view.command_line, "original prompt");
assert_eq!(editor.count(), Some(7));
assert_eq!(
editor.active.undo.iter().map(&snapshot).collect::<Vec<_>>(),
editor.active.undo.iter().map(&history).collect::<Vec<_>>(),
undo
);
assert_eq!(
editor.active.redo.iter().map(&snapshot).collect::<Vec<_>>(),
editor.active.redo.iter().map(&history).collect::<Vec<_>>(),
redo
);
assert_eq!(
@@ -12182,6 +12194,7 @@ fn palette_cancellation_preserves_every_underlying_mode_and_insert_group() {
}
let snapshot =
|state: &Snapshot| (state.lines.clone(), state.targets.clone(), state.primary);
let history = |entry: &HistoryEntry| (entry.start, entry.end, snapshot(&entry.before));
let lines = editor.active.lines.clone();
let targets = editor.active.view.targets.clone();
let primary = editor.active.view.primary;
@@ -12190,8 +12203,8 @@ fn palette_cancellation_preserves_every_underlying_mode_and_insert_group() {
let search = editor.active.view.search_query.clone();
let found = editor.active.view.search_match;
let message = editor.message.clone();
let undo: Vec<_> = editor.active.undo.iter().map(&snapshot).collect();
let redo: Vec<_> = editor.active.redo.iter().map(&snapshot).collect();
let undo: Vec<_> = editor.active.undo.iter().map(&history).collect();
let redo: Vec<_> = editor.active.redo.iter().map(&history).collect();
let insert = editor.active.view.insert_start.as_ref().map(snapshot);
editor.handle(Input::Ctrl('p'));
@@ -12218,11 +12231,11 @@ fn palette_cancellation_preserves_every_underlying_mode_and_insert_group() {
assert_eq!(editor.active.view.search_match, found);
assert_eq!(editor.message, message);
assert_eq!(
editor.active.undo.iter().map(&snapshot).collect::<Vec<_>>(),
editor.active.undo.iter().map(&history).collect::<Vec<_>>(),
undo
);
assert_eq!(
editor.active.redo.iter().map(&snapshot).collect::<Vec<_>>(),
editor.active.redo.iter().map(&history).collect::<Vec<_>>(),
redo
);
assert_eq!(
@@ -13489,6 +13502,56 @@ fn insert_sessions_and_open_lines_are_single_undo_actions() {
assert_eq!(editor.context().text, "hello\n世界");
}
#[test]
fn large_buffer_history_retains_changed_rows_and_round_trips_all_groups() {
let directory = Directory::new();
let path = directory.path("large.txt");
let source = format!("head\n{}tail\n", "unchanged contents\n".repeat(10_000));
fs::write(&path, &source).unwrap();
let mut editor = Editor::open(Some(path), vec![]).unwrap();
for _ in 0..12 {
editor.handle(Input::Char('i'));
editor.handle(Input::Char('界'));
editor.handle(Input::Escape);
}
assert_eq!(editor.lines()[0], format!("{}head", "界".repeat(12)));
let retained_bytes: usize = editor
.active
.undo
.iter()
.flat_map(|entry| &entry.before.lines)
.map(String::len)
.sum();
assert!(
retained_bytes < 4096,
"small edits retained {retained_bytes} bytes of history"
);
for _ in 0..12 {
editor.handle(Input::Char('u'));
}
assert_eq!(editor.lines().join("\n"), source);
assert!(!editor.dirty());
for _ in 0..12 {
editor.handle(Input::Char('U'));
}
assert_eq!(editor.lines()[0], format!("{}head", "界".repeat(12)));
assert_eq!(&editor.lines()[1..], &split_lines(&source)[1..]);
assert!(editor.dirty());
editor.handle(Input::Char('u'));
editor.handle(Input::Char('i'));
editor.handle(Input::Enter);
editor.handle(Input::Escape);
assert!(editor.active.redo.is_empty());
editor.handle(Input::Char('u'));
assert_eq!(editor.lines()[0], format!("{}head", "界".repeat(11)));
editor.execute("w");
assert!(!editor.dirty());
assert_eq!(
fs::read_to_string(editor.path().unwrap()).unwrap(),
format!("{}{source}", "界".repeat(11))
);
}
#[test]
fn grapheme_navigation_and_deletion_do_not_split_unicode() {
let mut editor = editor();
@@ -14076,6 +14139,52 @@ fn saves_preserve_empty_files_lf_trailing_newlines_and_dirty_history() {
assert!(editor.should_quit());
}
#[test]
fn dirty_cache_tracks_saved_text_across_edits_undo_restore_and_reload() {
let directory = Directory::new();
let path = directory.path("file.txt");
fs::write(&path, "saved\n").unwrap();
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
editor.set_clipboard_mode("separate").unwrap();
assert!(!editor.dirty());
editor
.apply(vec![Effect::Insert("changed".into())])
.unwrap();
assert!(editor.dirty());
for _ in 0..3 {
assert!(editor.dirty());
assert!(editor.active.info(true).dirty);
assert!(editor.active.document().dirty);
assert!(!editor.active.refresh_file().unwrap());
}
editor.handle(Input::Char('u'));
assert!(!editor.dirty());
editor.handle(Input::Char('U'));
assert!(editor.dirty());
editor
.apply(vec![Effect::Replace("saved\n".into())])
.unwrap();
assert!(!editor.dirty());
editor.apply(vec![Effect::Insert("new ".into())]).unwrap();
assert!(editor.dirty());
let revision = editor.text_revision();
editor.execute("w");
assert_eq!(editor.text_revision(), revision);
assert!(!editor.dirty());
editor.handle(Input::Char('u'));
assert!(editor.dirty());
editor.handle(Input::Char('U'));
assert!(!editor.dirty());
fs::write(&path, "reloaded content\n").unwrap();
assert!(editor.active.refresh_file().unwrap());
assert_eq!(editor.lines(), ["reloaded content", ""]);
assert!(!editor.dirty());
assert!(!editor.active.refresh_file().unwrap());
}
#[test]
fn save_as_and_failed_wq_keep_path_dirty_state_and_quit_correct() {
let directory = Directory::new();