//! Getting text out of the grid, for handing a region to another program. //! //! Shop's job is to own the buffer; searching, paging and editing it are things //! any program can do once it has the bytes. This module is the seam: it turns //! a range of rows into a string, and the binary hands that to whatever the //! user's runner config names. //! //! Rows are addressed ABSOLUTELY, not by viewport: row 0 is the oldest row //! still kept in scrollback and the last row is the bottom of the live screen. //! Viewport rows shift under the user every time output arrives, so a range //! expressed in them would mean something different by the time it was read. //! //! The text rules are [`Grid::selection_text`]'s, for the same reasons: a row //! that ran off the right edge joins the next with no break, and trailing //! blanks come off only the row that ends the line. Anything else invents //! characters the user never saw. use crate::{Cell, Grid}; impl Grid { /// Rows addressable in absolute space: scrollback plus the live screen. /// /// The alt screen keeps no history, so on it this is just the screen. A /// full-buffer emit while `vim` is up hands over what is on screen, which /// is what the user is looking at and all that is reachable anyway. pub fn abs_rows(&self) -> usize { self.abs_base() + self.rows() as usize } /// Absolute index of the live screen's top row. fn abs_base(&self) -> usize { if self.on_alt { 0 } else { self.history.len() } } /// The absolute row under visible row `r`. /// /// What "emit the visible screen" is expressed with: the viewport covers /// `abs_row_of_view(0)` up to `abs_row_of_view(0) + rows()`. pub fn abs_row_of_view(&self, r: u16) -> usize { // The viewport's top row sits `view_offset` rows above the live screen, // and `view_offset` never exceeds the history it points into. self.abs_base() + r as usize - self.view_offset() as usize } /// Absolute rows `start..end` as text, ready to write to a file. /// /// `end` is exclusive, and both ends are clamped, so /// `text_range(0, grid.abs_rows())` is the whole buffer and an over-long /// range is not an error. pub fn text_range(&self, start: usize, end: usize) -> String { let end = end.min(self.abs_rows()); let mut out = String::new(); for r in start..end { let wrapped = self.abs_row_wrapped(r) && r + 1 < end; let cells = self.abs_row(r); if wrapped { // Trailing blanks on a wrapped row are real cells the text ran // through. Stripping them would eat the space between two // words that happened to straddle the edge. self.push_row_text(cells, &mut out); } else { let mut line = String::new(); self.push_row_text(cells, &mut line); out.push_str(line.trim_end()); out.push('\n'); } } out } /// The cells of absolute row `r`, always exactly `cols` of them. fn abs_row(&self, r: usize) -> &[Cell] { match r.checked_sub(self.abs_base()) { Some(live) => { let start = self.row_start(live as u16); &self.active_cells()[start..start + self.cols() as usize] } None => &self.history[r].cells, } } /// Whether absolute row `r` ran off the right edge onto the next. fn abs_row_wrapped(&self, r: usize) -> bool { match r.checked_sub(self.abs_base()) { Some(live) => { let phys = self.phys_row(live as u16) as usize; let flags = if self.on_alt { &self.alt_wrapped } else { &self.main_wrapped }; flags.get(phys).copied().unwrap_or(false) } None => self.history[r].wrapped, } } } impl Grid { /// A row of cells as text: one entry per CHARACTER rather than per column, /// and each of those the whole cluster. /// /// A wide character's second column is not a character of its own and is /// skipped rather than emitted as the blank it holds; a character carrying /// combining marks comes out with them. fn push_row_text(&self, cells: &[Cell], out: &mut String) { for cell in cells.iter().filter(|c| !c.is_spacer()) { self.push_cell_text(cell, out); } } } #[cfg(test)] mod tests { use super::*; use shop_vt::Parser; /// A grid fed raw bytes, so tests can drive the deferred wrap and push rows /// into history the way real output does. fn grid_fed(cols: u16, rows: u16, bytes: &str) -> Grid { let mut grid = Grid::new(cols, rows); let mut parser = Parser::new(); parser.advance(&mut grid, bytes.as_bytes()); grid } fn all(g: &Grid) -> String { g.text_range(0, g.abs_rows()) } #[test] fn the_live_screen_comes_out_line_by_line() { let g = grid_fed(20, 3, "one\r\ntwo\r\nthree"); assert_eq!(all(&g), "one\ntwo\nthree\n"); } #[test] fn padding_is_trimmed_but_blank_rows_survive() { let g = grid_fed(20, 3, "top\r\n\r\nbottom"); assert_eq!(all(&g), "top\n\nbottom\n"); } #[test] fn a_wrapped_line_comes_out_as_one_line() { let g = grid_fed(6, 3, "abcdefghij"); assert_eq!(g.text_range(0, 2), "abcdefghij\n"); } #[test] fn a_row_that_filled_exactly_still_breaks() { // Six columns of text ended with CR/LF is not a wrap, even though the // cursor sat on the right edge. let g = grid_fed(6, 3, "abcdef\r\nghij"); assert_eq!(g.text_range(0, 2), "abcdef\nghij\n"); } #[test] fn a_space_straddling_the_wrap_survives() { // "ab" fills the row with real spaces before "cd" wraps onto the next. let g = grid_fed(6, 3, "ab cd"); assert_eq!(g.text_range(0, 2), "ab cd\n"); } #[test] fn three_rows_of_one_wrapped_line_come_out_as_one() { let g = grid_fed(4, 4, "0123456789ab"); assert_eq!(g.text_range(0, 3), "0123456789ab\n"); } #[test] fn the_blank_rows_under_the_output_emit_as_blank_lines() { // Two lines on a four-row screen. The tail is real screen the user is // looking at, so it comes out rather than being quietly trimmed. let g = grid_fed(20, 4, "one\r\ntwo"); assert_eq!(all(&g), "one\ntwo\n\n\n"); } #[test] fn scrollback_comes_out_ahead_of_the_screen() { // Three rows of screen, six lines printed: the first three are history. let g = grid_fed(20, 3, "one\r\ntwo\r\nthree\r\nfour\r\nfive\r\nsix"); assert_eq!(g.history_len(), 3); assert_eq!(g.abs_rows(), 6); assert_eq!(all(&g), "one\ntwo\nthree\nfour\nfive\nsix\n"); } #[test] fn a_range_takes_the_rows_it_names() { let g = grid_fed(20, 3, "one\r\ntwo\r\nthree\r\nfour\r\nfive\r\nsix"); assert_eq!(g.text_range(2, 4), "three\nfour\n"); assert_eq!(g.text_range(5, 99), "six\n", "an over-long end clamps"); assert_eq!(g.text_range(6, 6), ""); } #[test] fn a_range_ending_mid_wrap_does_not_run_on() { // The line wraps across rows 0 and 1; asking for row 0 alone must still // terminate, or the caller gets a file with no final newline. let g = grid_fed(6, 3, "abcdefghij"); assert_eq!(g.text_range(0, 1), "abcdef\n"); } #[test] fn the_visible_screen_is_a_range_like_any_other() { let mut g = grid_fed(20, 3, "one\r\ntwo\r\nthree\r\nfour\r\nfive\r\nsix"); let visible = |g: &Grid| { let top = g.abs_row_of_view(0); g.text_range(top, top + g.rows() as usize) }; assert_eq!(visible(&g), "four\nfive\nsix\n"); assert!(g.scroll_view_up(2)); assert_eq!(visible(&g), "two\nthree\nfour\n"); } #[test] fn the_alt_screen_emits_only_itself() { // Fill history on main, then switch to alt and draw something else. let g = grid_fed( 20, 3, "main1\r\nmain2\r\nmain3\r\nmain4\x1b[?1049h\x1b[Halt1\r\nalt2", ); assert_eq!(g.abs_rows(), 3, "history is unreachable from alt"); assert_eq!(all(&g), "alt1\nalt2\n\n"); } }