diff --git a/docs/development.md b/docs/development.md
index fe2fc0b..c64f7d6 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -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.
diff --git a/src/editor.rs b/src/editor.rs
index 6e17ed0..432947e 100644
--- a/src/editor.rs
+++ b/src/editor.rs
@@ -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()
diff --git a/src/tui.rs b/src/tui.rs
index 5bb3a50..325309d 100644
--- a/src/tui.rs
+++ b/src/tui.rs
@@ -1433,6 +1433,8 @@ fn keys_ready(
mut next: impl FnMut() -> io::Result