perf: avoid eager diff loading and full-file typing work

This commit is contained in:
2026-10-11 15:06:05 +02:00
parent 7b4bc80538
commit 2e05725a38
11 changed files with 1881 additions and 386 deletions
+175 -23
View File
@@ -347,7 +347,17 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
};
match input {
Event::Key(key) if key.kind != KeyEventKind::Release => {
if let Some(input) = translate(key) {
if session.editor().mode() == Mode::Insert && typing_key(&key) {
viewports.panels.follow_files();
pending = typing_ready(session, key, || {
if event::poll(Duration::ZERO)? {
event::read().map(Some)
} else {
Ok(None)
}
})?;
redraw = true;
} else if let Some(input) = translate(key) {
viewports.panels.follow_files();
session.handle(input);
redraw = true;
@@ -377,6 +387,36 @@ pub fn run(session: &mut EditorSession) -> io::Result<()> {
})
}
fn typing_key(key: &KeyEvent) -> bool {
key.kind != KeyEventKind::Release
&& matches!(key.code, KeyCode::Char(ch) if !ch.is_control())
&& !key
.modifiers
.intersects(KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER)
}
// Coalesce frames, not edits: bindings, snippets and history still see each key.
fn typing_ready(
session: &mut EditorSession,
mut key: KeyEvent,
mut next: impl FnMut() -> io::Result<Option<Event>>,
) -> io::Result<Option<Event>> {
for handled in 1..=64 {
session.handle(translate(key).expect("plain typing key"));
if handled == 64
|| session.editor().mode() != Mode::Insert
|| session.editor().should_quit()
{
return Ok(None);
}
match next()? {
Some(Event::Key(next)) if typing_key(&next) => key = next,
pending => return Ok(pending),
}
}
unreachable!()
}
fn translate(key: KeyEvent) -> Option<Input> {
if key
.modifiers
@@ -488,7 +528,6 @@ fn draw(
return;
}
panels.diff_syntax = None;
syntax.update(editor.lines(), editor.path(), editor.text_revision());
if area.width == 0 || area.height == 0 {
return;
}
@@ -568,6 +607,12 @@ fn draw(
*left = column + 1 - width;
}
}
syntax.update_visible(
editor.lines(),
editor.path(),
editor.text_revision(),
std::slice::from_ref(&(*top..top.saturating_add(height).min(editor.lines().len()))),
);
if gutter.width > 0 {
let numbers: Vec<Line> = (*top..top.saturating_add(height).min(editor.lines().len()))
.map(|row| {
@@ -1087,15 +1132,7 @@ fn draw_diff(
) {
let comparison = diff.comparison.as_ref();
let selected = comparison.and_then(|comparison| comparison.files.get(diff.file));
if diff.phase == Phase::View
&& let Some(id) = diff.comparison_id
&& let Some(file) = selected
{
panels
.diff_syntax
.get_or_insert_with(DiffSyntaxCache::default)
.update(id, diff.file, file, diff.live_revision);
} else {
if diff.phase != Phase::View || !diff.selected_file_loaded() {
panels.diff_syntax = None;
}
if area.width == 0 || area.height == 0 {
@@ -1297,7 +1334,7 @@ fn draw_diff(
.and_then(|syntax| syntax.before.error().or_else(|| syntax.after.error()));
let notice = diff.error.as_deref().or_else(|| {
if diff.loading {
Some("Loading comparison…")
Some("Loading file…")
} else {
syntax_error.or_else(|| selected.and_then(|file| file.notice.as_deref()))
}
@@ -1413,6 +1450,20 @@ fn draw_diff(
*left = column + 1 - width;
}
}
if let Some(id) = diff.comparison_id
&& let Some(file) = selected.filter(|file| file.loaded)
{
panels
.diff_syntax
.get_or_insert_with(DiffSyntaxCache::default)
.update(
id,
diff.file,
file,
diff.live_revision,
Some((diff, top, [before_area, after_area])),
);
}
if let Some(picker) = &diff.files
&& let Some(navigator) = navigator
{
@@ -1435,7 +1486,7 @@ fn draw_diff(
})
})
.map(|geometry| geometry.text_widths);
if let Some(file) = selected
if let Some(file) = selected.filter(|file| file.loaded)
&& let Some(syntax) = &panels.diff_syntax
{
if pane_layout == PaneLayout::Empty {
@@ -1505,7 +1556,9 @@ fn draw_diff(
} else {
frame.render_widget(
Paragraph::new(if diff.loading {
"Loading comparison…"
"Loading file…"
} else if selected.is_some() {
"File details unavailable · select the file to retry"
} else {
"No changed files"
}),
@@ -1799,6 +1852,8 @@ struct DiffSyntaxCache {
before_revision: u64,
before: SyntaxCache,
after: SyntaxCache,
before_rows: Vec<std::ops::Range<usize>>,
after_rows: Vec<std::ops::Range<usize>>,
}
impl DiffSyntaxCache {
@@ -1808,6 +1863,7 @@ impl DiffSyntaxCache {
file_index: usize,
file: &ChangedFile,
live_revision: Option<u64>,
visible: Option<(&DiffState, usize, [Rect; 2])>,
) {
let key = Some((comparison_id, file_index, live_revision));
if self.key.map(|(id, index, _)| (id, index)) != Some((comparison_id, file_index)) {
@@ -1823,13 +1879,64 @@ impl DiffSyntaxCache {
.checked_add(1)
.expect("diff syntax revisions exhausted");
}
self.before.update(
&file.before,
Some(file.old_path.as_deref().unwrap_or(&file.path)),
self.before_revision,
);
self.after
.update(&file.after, Some(&file.path), self.revision);
if let Some((diff, top, areas)) = visible {
diff_visible_rows(diff, top, areas[0], true, &mut self.before_rows);
diff_visible_rows(diff, top, areas[1], false, &mut self.after_rows);
self.before.update_visible(
&file.before,
Some(file.old_path.as_deref().unwrap_or(&file.path)),
self.before_revision,
&self.before_rows,
);
self.after.update_visible(
&file.after,
Some(&file.path),
self.revision,
&self.after_rows,
);
} else {
self.before.update(
&file.before,
Some(file.old_path.as_deref().unwrap_or(&file.path)),
self.before_revision,
);
self.after
.update(&file.after, Some(&file.path), self.revision);
}
}
}
fn diff_visible_rows(
diff: &DiffState,
top: usize,
pane: Rect,
before: bool,
ranges: &mut Vec<std::ops::Range<usize>>,
) {
ranges.clear();
let height = if pane.width >= 4 && pane.height >= 4 {
pane.height - 2
} else {
pane.height
};
for row in diff.display_rows.iter().skip(top).take(usize::from(height)) {
let DisplayRow::Line(index) = *row else {
continue;
};
let source = if before {
diff.rows[index].before
} else {
diff.rows[index].after
};
if let Some(source) = source {
if let Some(last) = ranges.last_mut()
&& last.end == source
{
last.end += 1;
} else {
ranges.push(source..source + 1);
}
}
}
}
@@ -2713,6 +2820,7 @@ mod tests {
let before: Vec<String> = before.split('\n').map(str::to_owned).collect();
let after: Vec<String> = after.split('\n').map(str::to_owned).collect();
ChangedFile {
loaded: true,
before_eof: false,
after_eof: false,
path: path.into(),
@@ -2759,13 +2867,13 @@ mod tests {
let mut file = syntax_diff_file("new.rs", "return 42", "fn main() {}");
file.old_path = Some("old.py".into());
let mut syntax = DiffSyntaxCache::default();
syntax.update(1, 0, &file, None);
syntax.update(1, 0, &file, None, None);
assert_eq!(syntax.before.language(), Some("Python"));
assert_eq!(syntax.after.language(), Some("Rust"));
assert_eq!(syntax.before.line(0)[0].kind, SyntaxKind::Keyword);
assert_eq!(syntax.after.line(0)[0].kind, SyntaxKind::Keyword);
file.path = "unknown.extension".into();
syntax.update(1, 0, &file, None);
syntax.update(1, 0, &file, None, None);
assert_eq!(syntax.after.language(), None);
assert!(syntax.after.line(0).is_empty());
@@ -2877,6 +2985,7 @@ mod tests {
fn deletion_file() -> ChangedFile {
ChangedFile {
loaded: true,
before_eof: false,
after_eof: false,
path: "deleted-lines.txt".into(),
@@ -3075,6 +3184,7 @@ mod tests {
after[30] = "let first_change = 900;".into();
after[60] = "let second_change = 901;".into();
let mut diff = diff_comparison(vec![ChangedFile {
loaded: true,
before_eof: false,
after_eof: false,
path: "excerpts.rs".into(),
@@ -3182,6 +3292,7 @@ mod tests {
#[test]
fn diff_added_rows_hatch_missing_before_but_tint_real_empty_after_lines() {
let mut diff = diff_comparison(vec![ChangedFile {
loaded: true,
before_eof: false,
after_eof: false,
path: "added.txt".into(),
@@ -3237,6 +3348,7 @@ mod tests {
#[test]
fn diff_changed_backgrounds_preserve_syntax_and_clipped_source_cells() {
let mut diff = diff_comparison(vec![ChangedFile {
loaded: true,
before_eof: false,
after_eof: false,
path: "changed.rs".into(),
@@ -3344,6 +3456,7 @@ mod tests {
#[test]
fn diff_view_changes_files_focus_and_keeps_horizontal_offsets_independent() {
let added = ChangedFile {
loaded: true,
before_eof: false,
after_eof: false,
path: "new-name.txt".into(),
@@ -3624,6 +3737,7 @@ mod tests {
assert!(screen_text(&terminal).contains("No changed files"));
assert!(screen_text(&terminal).contains("Explorer · Tree"));
let binary = diff_comparison(vec![ChangedFile {
loaded: true,
before_eof: false,
after_eof: false,
path: "image.bin".into(),
@@ -3637,6 +3751,7 @@ mod tests {
render_diff(&mut terminal, &binary, &mut panels);
assert!(screen_text(&terminal).contains("Binary file"));
let blank = diff_comparison(vec![ChangedFile {
loaded: true,
before_eof: false,
after_eof: false,
path: "empty.txt".into(),
@@ -4340,6 +4455,10 @@ mod tests {
assert_eq!(diff.file, 0);
session.handle(Input::Enter);
assert_eq!(session.editor().diff().unwrap().file, 3);
ready(&mut session, Phase::View);
terminal
.draw(|frame| viewports.draw_session(frame, &mut session))
.unwrap();
assert!(viewports.scroll(
&mut session,
document_wheel(MouseEventKind::ScrollDown, before.x + 1, before.y + 1),
@@ -7510,6 +7629,39 @@ end
assert!(terminal.backend().cursor_visible());
}
#[test]
fn queued_typing_preserves_graphemes_history_and_mode_barriers() {
let directory = Directory::new();
let path = directory.file("input.txt", "seed");
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
session.handle(Input::Char('i'));
let mut events = [
Event::Key(KeyEvent::new(KeyCode::Char('界'), KeyModifiers::NONE)),
Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)),
Event::Key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE)),
]
.into_iter();
let pending = typing_ready(
&mut session,
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
|| Ok(events.next()),
)
.unwrap();
assert_eq!(session.editor().lines(), ["a界seed"]);
assert_eq!(session.editor().mode(), Mode::Insert);
assert!(matches!(pending, Some(Event::Key(key)) if key.code == KeyCode::Esc));
session.handle(Input::Escape);
let Some(Event::Key(key)) = events.next() else {
panic!("remaining command")
};
session.handle(translate(key).unwrap());
assert_eq!(session.editor().lines(), ["aseed"]);
session.handle(Input::Char('u'));
assert_eq!(session.editor().lines(), ["a界seed"]);
session.handle(Input::Char('u'));
assert_eq!(session.editor().lines(), ["seed"]);
}
#[test]
fn queued_wheel_input_renders_latest_view_without_crossing_key_or_resize_barriers() {
let directory = Directory::new();