fix: coalesce rapid viewport scrolling redraws
This commit is contained in:
@@ -76,6 +76,7 @@ Diff styling checks cover independent snapshot language/shebang/injection detect
|
||||
|
||||
- Undo uses full-buffer snapshots and movement scans the current line. This favors straightforward correctness over large-file performance.
|
||||
- Rapid-wheel verification must replay bursts against the running editor and measure arrival at the final viewport after input stops, not just individual frame duration. Cheap frames can still accumulate a visible backlog if each queued report requires its own redraw.
|
||||
- Keyboard viewport scrolling coalesces up to 64 consecutive inputs for the same resolved scroll action into one redraw. Reversals, changes between row/half-page scrolling, and cursor motions end the batch so boundary clamping and caret reveal happen before the next action. Burst checks must include direction changes at the start/end of the buffer, not just throughput.
|
||||
- PTY throughput alone cannot prove terminal presentation stays smooth: capture the editor and terminal while the user reproduces a pause. Synchronized-output commands must not reach the terminal during frame computation, even when individual frames and queued-input catch-up appear fast.
|
||||
- Annotation coordinate validation fast-paths ASCII grapheme lengths using byte lengths with CRLF pair accounting, retaining Unicode segmentation for non-ASCII text. Validation remains atomic and still checks every token; large Unicode buffers or diagnostic batches can require foreground work.
|
||||
- Legacy synchronous plugin invocation may pause input for up to 500 ms before a host timeout. Async plugins opt into background replies; pending requests and transport queues are bounded. Config/plugin startup has a 2-second deadline per host. Buffer context/document snapshots must fit within a 4 MiB JSON frame; a too-large frame disables that plugin, not editing.
|
||||
|
||||
@@ -4457,6 +4457,10 @@ impl Editor {
|
||||
| Action::Right
|
||||
| Action::Down
|
||||
| Action::Up
|
||||
| Action::ScrollUp
|
||||
| Action::ScrollDown
|
||||
| Action::ScrollHalfUp
|
||||
| Action::ScrollHalfDown
|
||||
| Action::LineStart
|
||||
| Action::LineEnd
|
||||
| Action::DocumentStart
|
||||
@@ -4472,6 +4476,18 @@ impl Editor {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn viewport_scroll_action(&self, input: &Input) -> Option<&'static str> {
|
||||
match self.bindings.get(&self.active.view.mode)?.get(input)? {
|
||||
Binding::Action(
|
||||
action @ (Action::ScrollUp
|
||||
| Action::ScrollDown
|
||||
| Action::ScrollHalfUp
|
||||
| Action::ScrollHalfDown),
|
||||
) => Some(action.entry().name),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff_editable(&self) -> bool {
|
||||
self.diff().is_some_and(|diff| {
|
||||
self.diff_buffer_focus()
|
||||
|
||||
+73
@@ -1433,6 +1433,8 @@ fn keys_ready(
|
||||
mut next: impl FnMut() -> io::Result<Option<Event>>,
|
||||
) -> io::Result<Option<Event>> {
|
||||
let mode = session.editor().mode();
|
||||
// Render before reversing/clamping a scroll or revealing a moved caret.
|
||||
let scroll = session.editor().viewport_scroll_action(&input);
|
||||
for handled in 1..=64 {
|
||||
session.handle(input);
|
||||
if handled == 64 || session.editor().mode() != mode || session.editor().should_quit() {
|
||||
@@ -1442,6 +1444,7 @@ fn keys_ready(
|
||||
if let Some(Event::Key(key)) = &pending
|
||||
&& let Some(next) = translate(*key)
|
||||
&& batch_key(session.editor(), key, &next)
|
||||
&& session.editor().viewport_scroll_action(&next) == scroll
|
||||
{
|
||||
input = next;
|
||||
} else {
|
||||
@@ -17998,6 +18001,76 @@ end
|
||||
assert!(terminal.backend().cursor_visible());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_scrolls_coalesce_but_preserve_reversals_and_motion_barriers() {
|
||||
let directory = Directory::new();
|
||||
let text = (0..100)
|
||||
.map(|row| format!("row-{row}\n"))
|
||||
.collect::<String>();
|
||||
for (first, repeated, barrier) in [
|
||||
(
|
||||
Input::Ctrl('u'),
|
||||
'u',
|
||||
KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL),
|
||||
),
|
||||
(
|
||||
Input::Ctrl('d'),
|
||||
'd',
|
||||
KeyEvent::new(KeyCode::Home, KeyModifiers::NONE),
|
||||
),
|
||||
(
|
||||
Input::Down,
|
||||
'j',
|
||||
KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL),
|
||||
),
|
||||
] {
|
||||
let mut session =
|
||||
EditorSession::open(Some(directory.file("rows.txt", &text)), vec![], None).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
let modifiers = if repeated == 'j' {
|
||||
KeyModifiers::NONE
|
||||
} else {
|
||||
KeyModifiers::CONTROL
|
||||
};
|
||||
let mut events = std::iter::repeat_n(
|
||||
Event::Key(KeyEvent::new(KeyCode::Char(repeated), modifiers)),
|
||||
7,
|
||||
)
|
||||
.chain([Event::Key(barrier)]);
|
||||
assert_eq!(
|
||||
keys_ready(&mut session, first, || Ok(events.next())).unwrap(),
|
||||
Some(Event::Key(barrier))
|
||||
);
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
if repeated == 'u' {
|
||||
assert_eq!(views.offsets[&session.editor().buffer_id()].0, 0);
|
||||
session.handle(Input::Ctrl('d'));
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
views.offsets[&session.editor().buffer_id()].0,
|
||||
usize::from(views.document.geometry.unwrap().1.height) / 2
|
||||
);
|
||||
} else if repeated == 'd' {
|
||||
assert_eq!(views.offsets[&session.editor().buffer_id()].0, 40);
|
||||
session.handle(Input::Home);
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert_eq!(views.offsets[&session.editor().buffer_id()].0, 0);
|
||||
} else {
|
||||
assert_eq!(session.editor().cursor().row, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_navigation_preserves_counted_motions_visual_ranges_and_remaps() {
|
||||
let directory = Directory::new();
|
||||
|
||||
Reference in New Issue
Block a user