From 937868dd85584c2b343188e738fa972ab109c0a6 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sat, 11 Jul 2026 20:26:15 +0200 Subject: [PATCH] fix(editor): reveal the pasted block when it runs past the fold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A multi-line paste near the bottom of the screen left its later lines below the viewport — adjust_scroll only kept the caret's (first) pasted line visible. Add reveal(), which scrolls the end of the pasted block into view while the caret stays on its first line (vim semantics unchanged). --- editor/src/lib.rs | 69 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 3031398..35d6e33 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -1036,24 +1036,32 @@ impl Editor { } self.checkpoint(); let content = self.register.repeat(n); - if self.register_linewise { + // `end`: byte offset of the last pasted char, so the viewport can reveal + // the whole block even when the caret stays on its first line. + let end = if self.register_linewise { let le = self.line_end(self.caret); if le < self.text.len() { let at = le + 1; // start of the following line self.text.insert_str(at, &content); self.caret = at; + at + content.len() - 1 } else { // Last line has no trailing newline: prefix one, drop the // block's trailing newline so we don't leave a blank line. let block = content.strip_suffix('\n').unwrap_or(&content); - self.text.insert_str(le, &format!("\n{block}")); + let inserted = format!("\n{block}"); + let end = le + inserted.len() - 1; + self.text.insert_str(le, &inserted); self.caret = le + 1; + end } } else { let at = if self.text.is_empty() { 0 } else { self.next_char(self.caret) }; self.text.insert_str(at, &content); self.caret = self.prev_char(at + content.len()); // onto the last char - } + self.caret + }; + self.reveal(end); } /// `P` — paste the register `n` times before the caret. Linewise content @@ -1065,15 +1073,18 @@ impl Editor { } self.checkpoint(); let content = self.register.repeat(n); - if self.register_linewise { + let end = if self.register_linewise { let ls = self.line_start(self.caret); self.text.insert_str(ls, &content); self.caret = ls; + ls + content.len() - 1 } else { let at = self.caret; self.text.insert_str(at, &content); self.caret = self.prev_char(at + content.len()); // onto the last char - } + self.caret + }; + self.reveal(end); } /// Apply a pending operator over the buffer range `[start, end)` (order @@ -1349,6 +1360,32 @@ impl Editor { (row, col) } + /// Scroll so the display row holding byte offset `pos` is visible, + /// bottom-aligning it when it sits below the viewport (never scrolls up). + /// Called after an insert that can run past the fold — chiefly a multi-line + /// paste — so the *whole* pasted block is revealed, not just the caret's + /// first line. The caret is left where the edit put it; `draw`'s + /// `adjust_scroll` won't override this as long as the caret stays within the + /// resulting window (true for any block up to a screen tall). + fn reveal(&mut self, pos: usize) { + let lay = self.layout(); + if lay.is_empty() { + return; + } + let pos = pos.min(self.text.len()); + let mut row = 0; + for (i, l) in lay.iter().enumerate() { + if l.start <= pos { + row = i; + } else { + break; + } + } + if row >= self.scroll_top + ROWS { + self.scroll_top = row + 1 - ROWS; + } + } + /// Move the viewport so the caret stays visible (Normal/Insert), or just /// clamp it to the content (View). fn adjust_scroll(&mut self, caret_row: usize, total: usize) { @@ -2239,6 +2276,28 @@ mod tests { assert_eq!(e.text(), "abc"); } + #[test] + fn multiline_paste_at_the_bottom_reveals_the_whole_block() { + // A screenful+ of lines, caret on the last line; paste two lines after + // it. Both pasted lines must be visible without a manual scroll — the + // caret stays on the first pasted line, but the viewport reveals the end. + let mut e = Editor::with_text(vec!["x"; 20].join("\n")); // 20 display rows + e.handle(Key::Char('g')); + e.handle(Key::Char('g')); + e.handle(Key::Char('2')); + e.handle(Key::Char('y')); + e.handle(Key::Char('y')); // yank two lines + e.handle(Key::Char('G')); // to the last line + e.handle(Key::Char('p')); // paste two lines below it (22 rows total) + e.draw(true); // adjust_scroll runs; reveal already applied by paste + let last_row = e.layout().len() - 1; // the second pasted line + assert!( + last_row >= e.scroll_top() && last_row < e.scroll_top() + ROWS, + "pasted block end (row {last_row}) off-screen at scroll_top {}", + e.scroll_top() + ); + } + // ---- Undo / redo (v0.3) ---- #[test]