perf: batch queued cursor navigation events

This commit is contained in:
Valdemar Lorenzen
2026-10-14 15:06:04 +02:00
parent 1cb9a0ddfc
commit f26237af78
5 changed files with 187 additions and 25 deletions
+39
View File
@@ -2964,6 +2964,45 @@ impl Editor {
self.active.view.mode
}
}
/// Direct document motions can share a frame without bypassing key dispatch.
pub(crate) fn can_batch_navigation(&self, input: &Input) -> bool {
matches!(self.active.view.mode, Mode::Normal | Mode::Visual)
&& self.document_actions_available()
&& self.startup_diagnostic.is_none()
&& self.hover.is_none()
&& self.palette.is_none()
&& self.completion.is_none()
&& !self
.file_picker
.as_ref()
.is_some_and(|picker| picker.focused)
&& self.sequence_pending().is_none()
&& !self.g_pending
&& self.line_pending.is_none()
&& self.seek_pending.is_none()
&& matches!(
self.bindings
.get(&self.active.view.mode)
.and_then(|keys| keys.get(input)),
Some(Binding::Action(
Action::Left
| Action::Right
| Action::Down
| Action::Up
| Action::LineStart
| Action::LineEnd
| Action::DocumentStart
| Action::DocumentEnd
| Action::GoToLine
| Action::WordNext
| Action::WordPrevious
| Action::WordEnd
| Action::BigWordNext
| Action::BigWordPrevious
| Action::BigWordEnd
))
)
}
pub fn diff_editable(&self) -> bool {
self.diff
+141 -25
View File
@@ -846,15 +846,15 @@ pub fn run(session: &mut EditorSession, mut clear_screen: bool) -> io::Result<()
};
match input {
Event::Key(key) if key.kind != KeyEventKind::Release => {
if session.editor().mode() == Mode::Insert && typing_key(&key) {
if let Some(input) = translate(key) {
viewports.panels.follow_files();
pending = typing_ready(session, key, || {
next_terminal_event(&mut colors, Duration::ZERO)
})?;
redraw = true;
} else if let Some(input) = translate(key) {
viewports.panels.follow_files();
session.handle(input);
if batch_key(session.editor(), &key, &input) {
pending = keys_ready(session, input, || {
next_terminal_event(&mut colors, Duration::ZERO)
})?;
} else {
session.handle(input);
}
redraw = true;
}
}
@@ -913,24 +913,34 @@ fn typing_key(key: &KeyEvent) -> bool {
| KeyModifiers::META,
)
}
fn batch_key(editor: &Editor, key: &KeyEvent, input: &Input) -> bool {
if editor.mode() == Mode::Insert {
typing_key(key)
} else {
key.kind != KeyEventKind::Release && editor.can_batch_navigation(input)
}
}
// Coalesce frames, not edits: bindings, snippets and history still see each key.
fn typing_ready(
// Coalesce frames, not actions: bindings, selections and history still see each key.
fn keys_ready(
session: &mut EditorSession,
mut key: KeyEvent,
mut input: Input,
mut next: impl FnMut() -> io::Result<Option<Event>>,
) -> io::Result<Option<Event>> {
let mode = session.editor().mode();
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()
{
session.handle(input);
if handled == 64 || session.editor().mode() != mode || session.editor().should_quit() {
return Ok(None);
}
match next()? {
Some(Event::Key(next)) if typing_key(&next) => key = next,
pending => return Ok(pending),
let pending = next()?;
if let Some(Event::Key(key)) = &pending
&& let Some(next) = translate(*key)
&& batch_key(session.editor(), key, &next)
{
input = next;
} else {
return Ok(pending);
}
}
unreachable!()
@@ -15291,6 +15301,117 @@ end
assert!(terminal.backend().cursor_visible());
}
#[test]
fn queued_navigation_preserves_counted_motions_visual_ranges_and_remaps() {
let directory = Directory::new();
let mut rows = (0..100)
.map(|row| format!("row-{row:03}"))
.collect::<Vec<_>>();
rows[1].clear();
let text = rows.join("\n");
let path = directory.file("rows.txt", &text);
for mode in [Mode::Normal, Mode::Visual] {
let mut session = EditorSession::open(Some(path.clone()), vec![], None).unwrap();
session
.editor_mut()
.bind("normal", "z", Some("core.down".into()))
.unwrap();
session
.editor_mut()
.bind("visual", "z", Some("core.down".into()))
.unwrap();
session.handle(Input::End);
if mode == Mode::Visual {
session.handle(Input::Char('v'));
}
session.handle(Input::Char('3'));
let key = |code| Event::Key(KeyEvent::new(code, KeyModifiers::NONE));
let mut events = std::iter::once(key(KeyCode::Right))
.chain(std::iter::repeat_n(key(KeyCode::Down), 18))
.chain([
key(KeyCode::Up),
Event::Key(KeyEvent::new_with_kind(
KeyCode::Char('z'),
KeyModifiers::NONE,
KeyEventKind::Repeat,
)),
key(KeyCode::Char('i')),
key(KeyCode::Char('x')),
]);
let pending = keys_ready(&mut session, Input::Down, || Ok(events.next())).unwrap();
assert_eq!(session.editor().cursor(), Cursor { row: 21, column: 1 });
assert_eq!(session.editor().mode(), mode);
assert_eq!(session.editor().lines(), rows);
if mode == Mode::Visual {
assert_eq!(
session.editor().selections(),
vec![Selection {
start: Cursor { row: 0, column: 6 },
end: Cursor { row: 21, column: 2 },
}]
);
}
assert_eq!(pending, Some(key(KeyCode::Char('i'))));
assert_eq!(events.next(), Some(key(KeyCode::Char('x'))));
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(30, 8)).unwrap();
let mut views = BufferViewports::default();
terminal
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
assert!(screen_text(&terminal).contains("row-021"));
assert!(terminal.backend().cursor_visible());
}
}
#[test]
fn queued_navigation_stops_at_event_binding_and_batch_boundaries() {
let directory = Directory::new();
let text = (0..100)
.map(|row| format!("row-{row:03}\n"))
.collect::<String>();
let path = directory.file("rows.txt", &text);
let down = Event::Key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
for barrier in [
Event::Resize(40, 12),
Event::Paste("literal".into()),
Event::Mouse(document_wheel(MouseEventKind::ScrollDown, 1, 1)),
Event::Key(KeyEvent::new_with_kind(
KeyCode::Down,
KeyModifiers::NONE,
KeyEventKind::Release,
)),
Event::Key(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::NONE)),
Event::Key(KeyEvent::new(KeyCode::Char('z'), KeyModifiers::NONE)),
Event::Key(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE)),
] {
let mut session = EditorSession::open(Some(path.clone()), vec![], None).unwrap();
session
.editor_mut()
.bind("normal", "j", Some("core.insert".into()))
.unwrap();
session
.editor_mut()
.bind("normal", "z", Some("core.pane.split_horizontal".into()))
.unwrap();
let mut events = [barrier.clone(), down.clone()].into_iter();
let pending = keys_ready(&mut session, Input::Down, || Ok(events.next())).unwrap();
assert_eq!(pending, Some(barrier));
assert_eq!(events.next(), Some(down.clone()));
assert_eq!(session.editor().cursor(), Cursor { row: 1, column: 0 });
assert_eq!(session.editor().mode(), Mode::Normal);
assert_eq!(session.editor().pane_layout().len(), 1);
assert_eq!(session.editor().lines().join("\n"), text);
}
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
let mut events = std::iter::repeat_n(down, 80);
assert_eq!(
keys_ready(&mut session, Input::Down, || Ok(events.next())).unwrap(),
None
);
assert_eq!(session.editor().cursor(), Cursor { row: 64, column: 0 });
assert_eq!(events.count(), 17);
}
#[test]
fn queued_typing_preserves_graphemes_history_and_mode_barriers() {
let directory = Directory::new();
@@ -15303,12 +15424,7 @@ end
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();
let pending = keys_ready(&mut session, Input::Char('a'), || 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));