Skip to main content

max / shop

8.3 KB · 227 lines History Blame Raw
1 //! Getting text out of the grid, for handing a region to another program.
2 //!
3 //! Shop's job is to own the buffer; searching, paging and editing it are things
4 //! any program can do once it has the bytes. This module is the seam: it turns
5 //! a range of rows into a string, and the binary hands that to whatever the
6 //! user's runner config names.
7 //!
8 //! Rows are addressed ABSOLUTELY, not by viewport: row 0 is the oldest row
9 //! still kept in scrollback and the last row is the bottom of the live screen.
10 //! Viewport rows shift under the user every time output arrives, so a range
11 //! expressed in them would mean something different by the time it was read.
12 //!
13 //! The text rules are [`Grid::selection_text`]'s, for the same reasons: a row
14 //! that ran off the right edge joins the next with no break, and trailing
15 //! blanks come off only the row that ends the line. Anything else invents
16 //! characters the user never saw.
17
18 use crate::{Cell, Grid};
19
20 impl Grid {
21 /// Rows addressable in absolute space: scrollback plus the live screen.
22 ///
23 /// The alt screen keeps no history, so on it this is just the screen. A
24 /// full-buffer emit while `vim` is up hands over what is on screen, which
25 /// is what the user is looking at and all that is reachable anyway.
26 pub fn abs_rows(&self) -> usize {
27 self.abs_base() + self.rows() as usize
28 }
29
30 /// Absolute index of the live screen's top row.
31 fn abs_base(&self) -> usize {
32 if self.on_alt { 0 } else { self.history.len() }
33 }
34
35 /// The absolute row under visible row `r`.
36 ///
37 /// What "emit the visible screen" is expressed with: the viewport covers
38 /// `abs_row_of_view(0)` up to `abs_row_of_view(0) + rows()`.
39 pub fn abs_row_of_view(&self, r: u16) -> usize {
40 // The viewport's top row sits `view_offset` rows above the live screen,
41 // and `view_offset` never exceeds the history it points into.
42 self.abs_base() + r as usize - self.view_offset() as usize
43 }
44
45 /// Absolute rows `start..end` as text, ready to write to a file.
46 ///
47 /// `end` is exclusive, and both ends are clamped, so
48 /// `text_range(0, grid.abs_rows())` is the whole buffer and an over-long
49 /// range is not an error.
50 pub fn text_range(&self, start: usize, end: usize) -> String {
51 let end = end.min(self.abs_rows());
52 let mut out = String::new();
53 for r in start..end {
54 let wrapped = self.abs_row_wrapped(r) && r + 1 < end;
55 let cells = self.abs_row(r);
56 if wrapped {
57 // Trailing blanks on a wrapped row are real cells the text ran
58 // through. Stripping them would eat the space between two
59 // words that happened to straddle the edge.
60 self.push_row_text(cells, &mut out);
61 } else {
62 let mut line = String::new();
63 self.push_row_text(cells, &mut line);
64 out.push_str(line.trim_end());
65 out.push('\n');
66 }
67 }
68 out
69 }
70
71 /// The cells of absolute row `r`, always exactly `cols` of them.
72 fn abs_row(&self, r: usize) -> &[Cell] {
73 match r.checked_sub(self.abs_base()) {
74 Some(live) => {
75 let start = self.row_start(live as u16);
76 &self.active_cells()[start..start + self.cols() as usize]
77 }
78 None => &self.history[r].cells,
79 }
80 }
81
82 /// Whether absolute row `r` ran off the right edge onto the next.
83 fn abs_row_wrapped(&self, r: usize) -> bool {
84 match r.checked_sub(self.abs_base()) {
85 Some(live) => {
86 let phys = self.phys_row(live as u16) as usize;
87 let flags = if self.on_alt {
88 &self.alt_wrapped
89 } else {
90 &self.main_wrapped
91 };
92 flags.get(phys).copied().unwrap_or(false)
93 }
94 None => self.history[r].wrapped,
95 }
96 }
97 }
98
99 impl Grid {
100 /// A row of cells as text: one entry per CHARACTER rather than per column,
101 /// and each of those the whole cluster.
102 ///
103 /// A wide character's second column is not a character of its own and is
104 /// skipped rather than emitted as the blank it holds; a character carrying
105 /// combining marks comes out with them.
106 fn push_row_text(&self, cells: &[Cell], out: &mut String) {
107 for cell in cells.iter().filter(|c| !c.is_spacer()) {
108 self.push_cell_text(cell, out);
109 }
110 }
111 }
112
113 #[cfg(test)]
114 mod tests {
115 use super::*;
116 use shop_vt::Parser;
117
118 /// A grid fed raw bytes, so tests can drive the deferred wrap and push rows
119 /// into history the way real output does.
120 fn grid_fed(cols: u16, rows: u16, bytes: &str) -> Grid {
121 let mut grid = Grid::new(cols, rows);
122 let mut parser = Parser::new();
123 parser.advance(&mut grid, bytes.as_bytes());
124 grid
125 }
126
127 fn all(g: &Grid) -> String {
128 g.text_range(0, g.abs_rows())
129 }
130
131 #[test]
132 fn the_live_screen_comes_out_line_by_line() {
133 let g = grid_fed(20, 3, "one\r\ntwo\r\nthree");
134 assert_eq!(all(&g), "one\ntwo\nthree\n");
135 }
136
137 #[test]
138 fn padding_is_trimmed_but_blank_rows_survive() {
139 let g = grid_fed(20, 3, "top\r\n\r\nbottom");
140 assert_eq!(all(&g), "top\n\nbottom\n");
141 }
142
143 #[test]
144 fn a_wrapped_line_comes_out_as_one_line() {
145 let g = grid_fed(6, 3, "abcdefghij");
146 assert_eq!(g.text_range(0, 2), "abcdefghij\n");
147 }
148
149 #[test]
150 fn a_row_that_filled_exactly_still_breaks() {
151 // Six columns of text ended with CR/LF is not a wrap, even though the
152 // cursor sat on the right edge.
153 let g = grid_fed(6, 3, "abcdef\r\nghij");
154 assert_eq!(g.text_range(0, 2), "abcdef\nghij\n");
155 }
156
157 #[test]
158 fn a_space_straddling_the_wrap_survives() {
159 // "ab" fills the row with real spaces before "cd" wraps onto the next.
160 let g = grid_fed(6, 3, "ab cd");
161 assert_eq!(g.text_range(0, 2), "ab cd\n");
162 }
163
164 #[test]
165 fn three_rows_of_one_wrapped_line_come_out_as_one() {
166 let g = grid_fed(4, 4, "0123456789ab");
167 assert_eq!(g.text_range(0, 3), "0123456789ab\n");
168 }
169
170 #[test]
171 fn the_blank_rows_under_the_output_emit_as_blank_lines() {
172 // Two lines on a four-row screen. The tail is real screen the user is
173 // looking at, so it comes out rather than being quietly trimmed.
174 let g = grid_fed(20, 4, "one\r\ntwo");
175 assert_eq!(all(&g), "one\ntwo\n\n\n");
176 }
177
178 #[test]
179 fn scrollback_comes_out_ahead_of_the_screen() {
180 // Three rows of screen, six lines printed: the first three are history.
181 let g = grid_fed(20, 3, "one\r\ntwo\r\nthree\r\nfour\r\nfive\r\nsix");
182 assert_eq!(g.history_len(), 3);
183 assert_eq!(g.abs_rows(), 6);
184 assert_eq!(all(&g), "one\ntwo\nthree\nfour\nfive\nsix\n");
185 }
186
187 #[test]
188 fn a_range_takes_the_rows_it_names() {
189 let g = grid_fed(20, 3, "one\r\ntwo\r\nthree\r\nfour\r\nfive\r\nsix");
190 assert_eq!(g.text_range(2, 4), "three\nfour\n");
191 assert_eq!(g.text_range(5, 99), "six\n", "an over-long end clamps");
192 assert_eq!(g.text_range(6, 6), "");
193 }
194
195 #[test]
196 fn a_range_ending_mid_wrap_does_not_run_on() {
197 // The line wraps across rows 0 and 1; asking for row 0 alone must still
198 // terminate, or the caller gets a file with no final newline.
199 let g = grid_fed(6, 3, "abcdefghij");
200 assert_eq!(g.text_range(0, 1), "abcdef\n");
201 }
202
203 #[test]
204 fn the_visible_screen_is_a_range_like_any_other() {
205 let mut g = grid_fed(20, 3, "one\r\ntwo\r\nthree\r\nfour\r\nfive\r\nsix");
206 let visible = |g: &Grid| {
207 let top = g.abs_row_of_view(0);
208 g.text_range(top, top + g.rows() as usize)
209 };
210 assert_eq!(visible(&g), "four\nfive\nsix\n");
211 assert!(g.scroll_view_up(2));
212 assert_eq!(visible(&g), "two\nthree\nfour\n");
213 }
214
215 #[test]
216 fn the_alt_screen_emits_only_itself() {
217 // Fill history on main, then switch to alt and draw something else.
218 let g = grid_fed(
219 20,
220 3,
221 "main1\r\nmain2\r\nmain3\r\nmain4\x1b[?1049h\x1b[Halt1\r\nalt2",
222 );
223 assert_eq!(g.abs_rows(), 3, "history is unreachable from alt");
224 assert_eq!(all(&g), "alt1\nalt2\n\n");
225 }
226 }
227