perf: batch queued cursor navigation events
This commit is contained in:
+141
-25
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user