Skip to main content

max / shop

24.0 KB · 633 lines History Blame Raw
1 //! Editing the active screen: newline and the scrolling region, line and
2 //! column insert/delete, the erases, cursor save/restore, and the alt-screen
3 //! swap.
4 //!
5 //! Every entry point here is called from the CSI or ESC dispatch in
6 //! [`crate::perform`] and from nowhere else.
7
8 use crate::{Cell, Grid};
9
10 impl Grid {
11 pub(crate) fn newline(&mut self) {
12 if self.cursor.row < self.scroll_bottom {
13 self.cursor.row += 1;
14 } else {
15 self.scroll_up_in_region(1);
16 }
17 self.cursor.wrap_next = false;
18 // Cursor row moved, or the ring rotated under it — either way the
19 // cached row start is stale. place_char re-derives on next use.
20 self.invalidate_cur_row();
21 }
22
23 pub(crate) fn scroll_up_in_region(&mut self, n: u16) {
24 let region_size = self.scroll_bottom - self.scroll_top + 1;
25 let n = n.min(region_size);
26 if n == 0 {
27 return;
28 }
29 if self.is_partial_region() {
30 // Partial region: ring-rotate within the region. active_origin is
31 // guaranteed 0 in partial mode (unrolled on DECSTBM entry).
32 let rs = region_size as u32;
33 let old_region_origin = self.region_origin as u32;
34 self.region_origin = ((old_region_origin + n as u32) % rs) as u16;
35 for k in 0..n {
36 let phys_in_region = (old_region_origin + k as u32) % rs;
37 self.blank_physical_row(self.scroll_top + phys_in_region as u16);
38 }
39 // Partial-region scrolls don't propagate to the renderer's cache
40 // rotation; mark exposed rows dirty for rebuild.
41 for r in (self.scroll_bottom + 1 - n)..=self.scroll_bottom {
42 self.mark_row_dirty(r);
43 }
44 } else {
45 // Fullscreen fast path: O(1) origin shift + blank exposed rows.
46 let old_origin = self.active_origin();
47 self.advance_origin(n as i32);
48 for k in 0..n {
49 let phys = (old_origin as u32 + k as u32) % self.rows as u32;
50 // Before blanking, not after: this row is leaving the screen
51 // and the copy into history is the only thing that keeps it.
52 self.push_history(phys as u16);
53 self.blank_physical_row(phys as u16);
54 }
55 self.pending_scroll = self.pending_scroll.saturating_add(n as i16);
56 for r in (self.rows - n)..self.rows {
57 self.row_dirty[r as usize] = true;
58 }
59 }
60 }
61
62 pub(crate) fn scroll_down_in_region(&mut self, n: u16) {
63 let region_size = self.scroll_bottom - self.scroll_top + 1;
64 let n = n.min(region_size);
65 if n == 0 {
66 return;
67 }
68 if self.is_partial_region() {
69 let rs = region_size as u32;
70 let new_region_origin = ((self.region_origin as u32 + rs - n as u32) % rs) as u16;
71 self.region_origin = new_region_origin;
72 for k in 0..n {
73 let phys_in_region = (new_region_origin as u32 + k as u32) % rs;
74 self.blank_physical_row(self.scroll_top + phys_in_region as u16);
75 }
76 for r in self.scroll_top..(self.scroll_top + n) {
77 self.mark_row_dirty(r);
78 }
79 } else {
80 self.advance_origin(-(n as i32));
81 for k in 0..n {
82 self.blank_physical_row(
83 (self.active_origin() as u32 + k as u32) as u16 % self.rows,
84 );
85 }
86 self.pending_scroll = self.pending_scroll.saturating_sub(n as i16);
87 for r in 0..n {
88 self.row_dirty[r as usize] = true;
89 }
90 }
91 }
92
93 /// DECSC. Per-screen, so the alt screen's save cannot reach the main
94 /// screen's slot.
95 pub(crate) fn save_cursor(&mut self) {
96 self.dec_saved[self.on_alt as usize] = self.cursor;
97 }
98
99 /// DECRC. A restore with no matching save puts the cursor home, which is
100 /// what the default slot holds.
101 pub(crate) fn restore_cursor(&mut self) {
102 let mut c = self.dec_saved[self.on_alt as usize];
103 // The screen may have shrunk since the save. A cursor off the end of
104 // it is not a position anything can draw at.
105 c.row = c.row.min(self.rows - 1);
106 c.col = c.col.min(self.cols - 1);
107 self.cursor = c;
108 }
109
110 /// Whether logical row `r` of the active screen runs onto the next.
111 ///
112 /// Screen-relative, unlike the public [`row_wrapped`](Self::row_wrapped),
113 /// which takes a viewport row and may answer out of history. The row
114 /// movers below deal in screen rows, and reading the viewport's numbering
115 /// here would move the wrong flags whenever the user had scrolled back.
116 fn screen_row_wrapped(&self, r: u16) -> bool {
117 let phys = self.phys_row(r) as usize;
118 let flags = if self.on_alt {
119 &self.alt_wrapped
120 } else {
121 &self.main_wrapped
122 };
123 flags.get(phys).copied().unwrap_or(false)
124 }
125
126 /// Copy one whole screen row onto another, contents and wrap flag both.
127 ///
128 /// Goes through `row_start` per row rather than moving a span: logical
129 /// rows are a ring, so two rows adjacent on screen need not be adjacent in
130 /// memory, and a bulk move would shuffle the ring instead of the screen.
131 fn copy_row(&mut self, src: u16, dst: u16) {
132 if src == dst {
133 return;
134 }
135 let wrapped = self.screen_row_wrapped(src);
136 let (s, d) = (self.row_start(src), self.row_start(dst));
137 let cols = self.cols as usize;
138 self.active_cells_mut().copy_within(s..s + cols, d);
139 self.set_row_wrapped(dst, wrapped);
140 }
141
142 fn blank_screen_row(&mut self, r: u16) {
143 self.erase_line_range(r, 0, self.cols);
144 self.set_row_wrapped(r, false);
145 }
146
147 /// Blank any half of a wide pair whose other half is gone.
148 ///
149 /// The column movers shift a run of cells sideways, and a shift can cut a
150 /// pair in two: the lead of a wide character can be pushed off the right
151 /// edge, or a spacer can be pulled away from its lead. Either half left
152 /// alone is a cell lying about what it holds — the same reasoning
153 /// `erase_line_range` applies at its ends, applied to the whole row
154 /// because a shift can break a pair anywhere along it.
155 fn heal_wide_pairs(&mut self, row: u16) {
156 let start = self.row_start(row);
157 let cols = self.cols as usize;
158 let cells = &mut self.active_cells_mut()[start..start + cols];
159 for i in 0..cols {
160 let orphan_lead = cells[i].is_wide() && !cells.get(i + 1).is_some_and(Cell::is_spacer);
161 let orphan_spacer = cells[i].is_spacer() && !(i > 0 && cells[i - 1].is_wide());
162 if orphan_lead || orphan_spacer {
163 cells[i] = Cell::default();
164 }
165 }
166 }
167
168 /// IL. Open `n` blank lines at the cursor, pushing what follows down and
169 /// off the bottom of the scrolling region.
170 ///
171 /// Ignored when the cursor sits outside the region: the region is the part
172 /// of the screen the program has claimed, and an insert from outside it
173 /// would move rows it does not own.
174 pub(crate) fn insert_lines(&mut self, n: u16) {
175 if self.cursor.row < self.scroll_top || self.cursor.row > self.scroll_bottom {
176 return;
177 }
178 let top = self.cursor.row;
179 let n = n.min(self.scroll_bottom - top + 1);
180 if n == 0 {
181 return;
182 }
183 // Downward, so a row is read before the copy that overwrites it.
184 for r in (top + n..=self.scroll_bottom).rev() {
185 self.copy_row(r - n, r);
186 }
187 for r in top..top + n {
188 self.blank_screen_row(r);
189 }
190 // The row above the opening no longer runs into what is now a blank.
191 if top > 0 {
192 self.set_row_wrapped(top - 1, false);
193 }
194 for r in top..=self.scroll_bottom {
195 self.mark_row_dirty(r);
196 }
197 // DEC puts the cursor at the left margin, and enough programs rely on
198 // it that leaving the column alone is the surprising choice.
199 self.cursor.col = 0;
200 self.cursor.wrap_next = false;
201 }
202
203 /// DL. Remove `n` lines at the cursor, pulling the rest of the scrolling
204 /// region up and blanking what it vacates at the bottom.
205 pub(crate) fn delete_lines(&mut self, n: u16) {
206 if self.cursor.row < self.scroll_top || self.cursor.row > self.scroll_bottom {
207 return;
208 }
209 let top = self.cursor.row;
210 let n = n.min(self.scroll_bottom - top + 1);
211 if n == 0 {
212 return;
213 }
214 // Written as one exclusive boundary rather than two inclusive ranges,
215 // because `scroll_bottom - n` underflows when the delete covers the
216 // whole region from its top row: `n` is clamped to the region size, so
217 // n == scroll_bottom + 1 is reachable with top == 0. A panic in debug,
218 // and in release a range running to about 65,000 that hands `copy_row`
219 // rows off the end of the ring. `scroll_bottom + 1 - n` cannot
220 // underflow, since n is at most scroll_bottom - top + 1. Found by the
221 // soak oracle, 2026-08-29.
222 let keep_end = self.scroll_bottom + 1 - n;
223 for r in top..keep_end {
224 self.copy_row(r + n, r);
225 }
226 for r in keep_end..=self.scroll_bottom {
227 self.blank_screen_row(r);
228 }
229 if top > 0 {
230 self.set_row_wrapped(top - 1, false);
231 }
232 for r in top..=self.scroll_bottom {
233 self.mark_row_dirty(r);
234 }
235 self.cursor.col = 0;
236 self.cursor.wrap_next = false;
237 }
238
239 /// ICH. Open `n` blank cells at the cursor, pushing the rest of the line
240 /// right and off the edge. The cursor does not move.
241 pub(crate) fn insert_chars(&mut self, n: u16) {
242 let row = self.cursor.row;
243 let col = self.cursor.col;
244 let cols = self.cols;
245 let n = n.min(cols - col);
246 if n == 0 {
247 return;
248 }
249 let start = self.row_start(row);
250 let (c, k, w) = (col as usize, n as usize, cols as usize);
251 let cells = &mut self.active_cells_mut()[start..start + w];
252 cells.copy_within(c..w - k, c + k);
253 for cell in &mut cells[c..c + k] {
254 *cell = Cell::default();
255 }
256 self.heal_wide_pairs(row);
257 // Whatever ran off the right edge is gone, so the line stops here.
258 self.set_row_wrapped(row, false);
259 self.mark_row_dirty(row);
260 }
261
262 /// DCH. Remove `n` cells at the cursor, pulling the rest of the line left
263 /// and blanking the tail it vacates.
264 pub(crate) fn delete_chars(&mut self, n: u16) {
265 let row = self.cursor.row;
266 let col = self.cursor.col;
267 let cols = self.cols;
268 let n = n.min(cols - col);
269 if n == 0 {
270 return;
271 }
272 let start = self.row_start(row);
273 let (c, k, w) = (col as usize, n as usize, cols as usize);
274 let cells = &mut self.active_cells_mut()[start..start + w];
275 cells.copy_within(c + k..w, c);
276 for cell in &mut cells[w - k..] {
277 *cell = Cell::default();
278 }
279 self.heal_wide_pairs(row);
280 self.set_row_wrapped(row, false);
281 self.mark_row_dirty(row);
282 }
283
284 /// ECH. Blank `n` cells from the cursor without moving anything. The
285 /// difference from DCH is the whole point: the tail of the line stays
286 /// where it is.
287 pub(crate) fn erase_chars(&mut self, n: u16) {
288 let row = self.cursor.row;
289 let col = self.cursor.col;
290 let end = col.saturating_add(n).min(self.cols);
291 self.erase_line_range(row, col, end);
292 self.mark_row_dirty(row);
293 }
294
295 pub(crate) fn erase_line(&mut self, mode: u16) {
296 let row = self.cursor.row;
297 let col = self.cursor.col;
298 let (start_col, end_col) = match mode {
299 1 => (0, col + 1), // start to cursor
300 2 => (0, self.cols), // whole line
301 _ => (col, self.cols), // 0: cursor to end
302 };
303 self.erase_line_range(row, start_col, end_col);
304 self.mark_row_dirty(row);
305 }
306
307 pub(crate) fn erase_display(&mut self, mode: u16) {
308 // Iterate LOGICAL rows — physical layout is a ring, so contiguous
309 // "erase from cursor to end" isn't contiguous in memory.
310 let cursor_row = self.cursor.row;
311 let cursor_col = self.cursor.col;
312 match mode {
313 1 => {
314 // Start of screen to cursor (inclusive).
315 for r in 0..cursor_row {
316 self.blank_logical_row(r);
317 }
318 self.erase_line_range(cursor_row, 0, cursor_col + 1);
319 }
320 2 | 3 => {
321 for r in 0..self.rows {
322 self.blank_logical_row(r);
323 }
324 }
325 _ => {
326 // Cursor to end of screen (inclusive).
327 self.erase_line_range(cursor_row, cursor_col, self.cols);
328 for r in (cursor_row + 1)..self.rows {
329 self.blank_logical_row(r);
330 }
331 }
332 }
333 self.mark_all_rows_dirty();
334 }
335
336 fn erase_line_range(&mut self, row: u16, start_col: u16, end_col: u16) {
337 let row_start = self.row_start(row);
338 let end_col = end_col.min(self.cols);
339 let start_col = start_col.min(end_col);
340 let cols = self.cols;
341 let cells = self.active_cells_mut();
342 for cell in &mut cells[row_start + start_col as usize..row_start + end_col as usize] {
343 *cell = Cell::default();
344 }
345 // An erase can start or stop in the middle of a wide character. The
346 // half outside the range goes too: half a character is not a narrower
347 // character, it is a cell lying about what it holds.
348 if start_col > 0 && cells[row_start + start_col as usize - 1].is_wide() {
349 cells[row_start + start_col as usize - 1] = Cell::default();
350 }
351 if end_col < cols && cells[row_start + end_col as usize].is_spacer() {
352 cells[row_start + end_col as usize] = Cell::default();
353 }
354 // Erasing through the right edge destroys whatever ran off it, so the
355 // row no longer continues onto the next.
356 if end_col == self.cols {
357 self.set_row_wrapped(row, false);
358 }
359 }
360
361 pub(crate) fn swap_alt(&mut self, to_alt: bool) {
362 if self.on_alt == to_alt {
363 return;
364 }
365 // region_origin belongs to the currently-active screen; unroll before
366 // switching so the other screen starts with region_origin = 0.
367 self.unroll_region();
368 // An application taking the alt screen is taking the whole window, so
369 // a viewport parked in main's history has nothing left to show. Going
370 // the other way, the user is put back where the shell is, not where
371 // they were reading before vim opened.
372 self.view_offset = 0;
373 if to_alt {
374 self.swap_saved[0] = self.cursor;
375 self.on_alt = true;
376 for cell in &mut self.alt {
377 *cell = Cell::default();
378 }
379 self.alt_wrapped.fill(false);
380 self.cursor = self.swap_saved[1];
381 } else {
382 self.swap_saved[1] = self.cursor;
383 self.on_alt = false;
384 self.cursor = self.swap_saved[0];
385 }
386 self.pending_screen_swap = true;
387 self.mark_all_rows_dirty();
388 }
389
390 pub(crate) fn set_cursor(&mut self, row: u16, col: u16) {
391 // Terminal params are 1-indexed; convert.
392 let row = row.saturating_sub(1).min(self.rows - 1);
393 let col = col.saturating_sub(1).min(self.cols - 1);
394 self.cursor.row = row;
395 self.cursor.col = col;
396 self.cursor.wrap_next = false;
397 }
398
399 pub(crate) fn move_by(&mut self, drow: i32, dcol: i32) {
400 let r = (self.cursor.row as i32 + drow).clamp(0, self.rows as i32 - 1) as u16;
401 let c = (self.cursor.col as i32 + dcol).clamp(0, self.cols as i32 - 1) as u16;
402 self.cursor.row = r;
403 self.cursor.col = c;
404 self.cursor.wrap_next = false;
405 }
406 }
407
408 #[cfg(test)]
409 mod tests {
410 use crate::testutil::{feed, reply_to, row_str};
411 use crate::*;
412
413 // ---- erase ---------------------------------------------------------
414
415 #[test]
416 fn el0_erases_cursor_to_end() {
417 let mut g = Grid::new(10, 2);
418 feed(&mut g, b"ABCDEFGHIJ\x1b[H\x1b[3C\x1b[K");
419 assert_eq!(row_str(&g, 0), "ABC");
420 }
421
422 #[test]
423 fn el1_erases_start_to_cursor() {
424 let mut g = Grid::new(10, 2);
425 feed(&mut g, b"ABCDEFGHIJ\x1b[H\x1b[3C\x1b[1K");
426 // Cells 0..=3 cleared, 4..=9 kept.
427 assert_eq!(row_str(&g, 0), " EFGHIJ");
428 }
429
430 #[test]
431 fn el2_erases_whole_line() {
432 let mut g = Grid::new(10, 2);
433 feed(&mut g, b"ABCDEFGHIJ\x1b[H\x1b[3C\x1b[2K");
434 assert_eq!(row_str(&g, 0), "");
435 }
436
437 #[test]
438 fn ed2_erases_whole_screen() {
439 let mut g = Grid::new(6, 3);
440 feed(&mut g, b"aaaaaa\r\nbbbbbb\r\ncccccc\x1b[2J");
441 for r in 0..3 {
442 assert_eq!(row_str(&g, r), "");
443 }
444 }
445
446 // ---- scroll region + newline --------------------------------------
447
448 #[test]
449 fn newline_at_bottom_scrolls_up() {
450 let mut g = Grid::new(6, 3);
451 feed(&mut g, b"aaa\r\nbbb\r\nccc\r\nddd");
452 // Last write scrolled: row 0 was aaa, is now bbb; row 1 ccc; row 2 ddd.
453 assert_eq!(row_str(&g, 0), "bbb");
454 assert_eq!(row_str(&g, 1), "ccc");
455 assert_eq!(row_str(&g, 2), "ddd");
456 }
457
458 #[test]
459 fn reverse_index_at_top_scrolls_down() {
460 let mut g = Grid::new(6, 3);
461 feed(&mut g, b"aaa\r\nbbb\r\nccc\x1b[H\x1bM");
462 // RI from row 0 pushes row 0 down; row 0 blanked.
463 assert_eq!(row_str(&g, 0), "");
464 assert_eq!(row_str(&g, 1), "aaa");
465 assert_eq!(row_str(&g, 2), "bbb");
466 }
467
468 #[test]
469 fn insert_lines_pushes_the_rest_down_and_off_the_bottom() {
470 let mut g = Grid::new(6, 4);
471 feed(&mut g, b"one\r\ntwo\r\nthree\r\nfour");
472 feed(&mut g, b"\x1b[2;1H\x1b[L"); // row 1, insert one line
473 assert_eq!(row_str(&g, 0), "one");
474 assert_eq!(row_str(&g, 1), "");
475 assert_eq!(row_str(&g, 2), "two");
476 assert_eq!(row_str(&g, 3), "three");
477 // "four" fell off the bottom rather than scrolling into history.
478 assert_eq!(g.history_len(), 0);
479 }
480
481 #[test]
482 fn insert_lines_moves_the_right_rows_after_a_scroll() {
483 // The ring's origin is non-zero here, so a row mover that walked
484 // memory instead of the logical rows would shuffle the wrong ones.
485 let mut g = Grid::new(6, 3);
486 feed(&mut g, b"one\r\ntwo\r\nthree\r\nfour\r\nfive");
487 assert_eq!(row_str(&g, 0), "three");
488 feed(&mut g, b"\x1b[1;1H\x1b[L");
489 assert_eq!(row_str(&g, 0), "");
490 assert_eq!(row_str(&g, 1), "three");
491 assert_eq!(row_str(&g, 2), "four");
492 }
493
494 #[test]
495 fn delete_lines_pulls_the_rest_up_and_blanks_the_bottom() {
496 let mut g = Grid::new(6, 4);
497 feed(&mut g, b"one\r\ntwo\r\nthree\r\nfour");
498 feed(&mut g, b"\x1b[2;1H\x1b[M");
499 assert_eq!(row_str(&g, 0), "one");
500 assert_eq!(row_str(&g, 1), "three");
501 assert_eq!(row_str(&g, 2), "four");
502 assert_eq!(row_str(&g, 3), "");
503 }
504
505 #[test]
506 fn line_edits_stay_inside_the_scrolling_region() {
507 let mut g = Grid::new(6, 5);
508 feed(&mut g, b"a\r\nb\r\nc\r\nd\r\ne");
509 feed(&mut g, b"\x1b[2;4r"); // region is rows 1..=3
510 feed(&mut g, b"\x1b[2;1H\x1b[M"); // delete at the region's top
511 assert_eq!(row_str(&g, 0), "a", "a row above the region moved");
512 assert_eq!(row_str(&g, 1), "c");
513 assert_eq!(row_str(&g, 2), "d");
514 assert_eq!(row_str(&g, 3), "", "the region's bottom did not blank");
515 assert_eq!(row_str(&g, 4), "e", "a row below the region moved");
516 }
517
518 #[test]
519 fn a_line_edit_outside_the_region_does_nothing() {
520 // The program claimed rows 1..=3; the cursor is on row 4. DEC ignores
521 // this rather than clamping, and clamping would edit rows the program
522 // said it was not touching.
523 let mut g = Grid::new(6, 5);
524 feed(&mut g, b"a\r\nb\r\nc\r\nd\r\ne");
525 feed(&mut g, b"\x1b[2;4r\x1b[5;1H\x1b[M");
526 assert_eq!(row_str(&g, 4), "e");
527 }
528
529 #[test]
530 fn a_line_edit_puts_the_cursor_at_the_left_margin() {
531 let mut g = Grid::new(8, 3);
532 feed(&mut g, b"\x1b[1;5H\x1b[L");
533 assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[1;1R");
534 }
535
536 #[test]
537 fn insert_chars_opens_a_gap_and_drops_the_tail() {
538 let mut g = Grid::new(6, 1);
539 feed(&mut g, b"abcdef");
540 feed(&mut g, b"\x1b[1;3H\x1b[2@");
541 assert_eq!(row_str(&g, 0), "ab cd");
542 }
543
544 #[test]
545 fn delete_chars_closes_the_gap_and_blanks_the_tail() {
546 let mut g = Grid::new(6, 1);
547 feed(&mut g, b"abcdef");
548 feed(&mut g, b"\x1b[1;3H\x1b[2P");
549 assert_eq!(row_str(&g, 0), "abef");
550 }
551
552 #[test]
553 fn erase_chars_blanks_in_place_without_moving_the_tail() {
554 // The whole difference from DCH: "ef" does not move left.
555 let mut g = Grid::new(6, 1);
556 feed(&mut g, b"abcdef");
557 feed(&mut g, b"\x1b[1;3H\x1b[2X");
558 assert_eq!(row_str(&g, 0), "ab ef");
559 }
560
561 #[test]
562 fn a_column_edit_past_the_end_of_the_row_stops_at_it() {
563 let mut g = Grid::new(6, 1);
564 feed(&mut g, b"abcdef");
565 feed(&mut g, b"\x1b[1;5H\x1b[99P");
566 assert_eq!(row_str(&g, 0), "abcd");
567 }
568
569 #[test]
570 fn a_delete_landing_on_a_wide_lead_takes_its_spacer_too() {
571 // A shift moves a pair whole, so the break needs a cut inside it: this
572 // deletes the lead and would otherwise pull the spacer up alone, a
573 // cell claiming to be the second half of nothing.
574 let mut g = Grid::new(6, 1);
575 feed(&mut g, "ab\u{4e00}c".as_bytes()); // the wide char takes cols 2-3
576 feed(&mut g, b"\x1b[1;3H\x1b[P");
577 assert!(
578 !g.row(0).iter().any(super::Cell::is_spacer),
579 "a spacer outlived its character"
580 );
581 assert_eq!(row_str(&g, 0), "ab c");
582 }
583
584 #[test]
585 fn an_insert_pushing_a_wide_lead_to_the_edge_drops_it() {
586 // The other half of the same rule: the spacer goes off the right edge
587 // and the lead cannot be two columns wide in one column.
588 let mut g = Grid::new(6, 1);
589 feed(&mut g, "ab\u{4e00}c".as_bytes());
590 feed(&mut g, b"\x1b[1;1H\x1b[3@");
591 let row: String = g.row(0).iter().map(super::Cell::c).collect();
592 assert!(
593 !row.contains('\u{4e00}'),
594 "half a wide character survived at the edge: {row:?}"
595 );
596 }
597
598 #[test]
599 fn decsc_and_decrc_keep_a_slot_per_screen() {
600 // The bug this pair of slots exists for: a save taken on the alt
601 // screen used to land in the slot that leaving alt restored from, so
602 // the shell's cursor moved to wherever the full-screen program's was.
603 let mut g = Grid::new(20, 10);
604 feed(&mut g, b"\x1b[5;5H\x1b7"); // main: park at 5,5 and save
605 feed(&mut g, b"\x1b[?1049h"); // into alt
606 feed(&mut g, b"\x1b[9;9H\x1b7"); // alt: save somewhere else
607 feed(&mut g, b"\x1b[?1049l"); // back to main
608 assert_eq!(
609 reply_to(&mut g, b"\x1b[6n"),
610 "\x1b[5;5R",
611 "the swap lost it"
612 );
613 feed(&mut g, b"\x1b[1;1H\x1b8"); // main's DECRC
614 assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[5;5R");
615 }
616
617 #[test]
618 fn the_csi_spelling_of_save_and_restore_is_the_same_pair() {
619 let mut g = Grid::new(20, 10);
620 feed(&mut g, b"\x1b[4;7H\x1b[s\x1b[1;1H\x1b[u");
621 assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[4;7R");
622 }
623
624 #[test]
625 fn a_restore_onto_a_smaller_screen_lands_on_it() {
626 let mut g = Grid::new(20, 10);
627 feed(&mut g, b"\x1b[9;18H\x1b7");
628 g.resize(8, 4);
629 feed(&mut g, b"\x1b8");
630 assert_eq!(reply_to(&mut g, b"\x1b[6n"), "\x1b[4;8R");
631 }
632 }
633