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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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::Right
|
||||||
| Action::Down
|
| Action::Down
|
||||||
| Action::Up
|
| Action::Up
|
||||||
|
| Action::ScrollUp
|
||||||
|
| Action::ScrollDown
|
||||||
|
| Action::ScrollHalfUp
|
||||||
|
| Action::ScrollHalfDown
|
||||||
| Action::LineStart
|
| Action::LineStart
|
||||||
| Action::LineEnd
|
| Action::LineEnd
|
||||||
| Action::DocumentStart
|
| 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 {
|
pub fn diff_editable(&self) -> bool {
|
||||||
self.diff().is_some_and(|diff| {
|
self.diff().is_some_and(|diff| {
|
||||||
self.diff_buffer_focus()
|
self.diff_buffer_focus()
|
||||||
|
|||||||
+73
@@ -1433,6 +1433,8 @@ fn keys_ready(
|
|||||||
mut next: impl FnMut() -> io::Result<Option<Event>>,
|
mut next: impl FnMut() -> io::Result<Option<Event>>,
|
||||||
) -> io::Result<Option<Event>> {
|
) -> io::Result<Option<Event>> {
|
||||||
let mode = session.editor().mode();
|
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 {
|
for handled in 1..=64 {
|
||||||
session.handle(input);
|
session.handle(input);
|
||||||
if handled == 64 || session.editor().mode() != mode || session.editor().should_quit() {
|
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
|
if let Some(Event::Key(key)) = &pending
|
||||||
&& let Some(next) = translate(*key)
|
&& let Some(next) = translate(*key)
|
||||||
&& batch_key(session.editor(), key, &next)
|
&& batch_key(session.editor(), key, &next)
|
||||||
|
&& session.editor().viewport_scroll_action(&next) == scroll
|
||||||
{
|
{
|
||||||
input = next;
|
input = next;
|
||||||
} else {
|
} else {
|
||||||
@@ -17998,6 +18001,76 @@ end
|
|||||||
assert!(terminal.backend().cursor_visible());
|
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]
|
#[test]
|
||||||
fn queued_navigation_preserves_counted_motions_visual_ranges_and_remaps() {
|
fn queued_navigation_preserves_counted_motions_visual_ranges_and_remaps() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user