perf: avoid eager diff loading and full-file typing work
This commit is contained in:
+175
-23
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user