1use crate::app::modals::copy_modal::thousands;
13use std::collections::HashMap;
14use std::sync::Arc;
15use unicode_width::UnicodeWidthChar;
16
17pub const SEG: usize = 2048;
19const BREAK_LOOKBACK: usize = 256;
21const TOKEN_RUN: usize = 512;
24const CACHE_UNITS: usize = 256;
26pub const MAX_HITS: usize = 100_000;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum Tone {
31 Plain,
32 Dim,
33 Warn,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum Wrap {
40 #[default]
41 Word,
42 Hard,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum TextForm {
49 Raw,
50 Escaped,
51}
52
53#[derive(Debug, Clone)]
55pub enum Content {
56 Lines(Vec<(String, Tone)>),
58 Text {
61 text: Arc<str>,
62 form: TextForm,
63 lines: usize,
64 },
65 Hex { bytes: Arc<[u8]>, per_line: usize },
67}
68
69impl Content {
70 pub fn text(text: Arc<str>, form: TextForm) -> Self {
71 let lines = match form {
72 TextForm::Raw => count_lines(&text),
73 TextForm::Escaped => 1,
74 };
75 Content::Text { text, form, lines }
76 }
77
78 pub fn wraps(&self) -> bool {
80 matches!(self, Content::Text { .. })
81 }
82}
83
84pub fn count_lines(text: &str) -> usize {
86 bytecount(text.as_bytes(), b'\n') + 1
87}
88
89fn bytecount(bytes: &[u8], needle: u8) -> usize {
90 bytes.iter().filter(|&&b| b == needle).count()
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Row {
96 pub text: String,
97 pub start: usize,
98 pub end: usize,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct Shown {
104 pub text: String,
105 pub tone: Tone,
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct Window {
111 pub rows: Vec<Shown>,
112 pub above: bool,
113 pub below: bool,
114 pub from: usize,
117 pub to: usize,
118}
119
120fn unit_bounds(text: &str, start: usize, lines: bool) -> (usize, usize) {
123 let bytes = text.as_bytes();
124 let len = bytes.len();
125 let lim = len.min(start.saturating_add(SEG));
126 if lines && let Some(i) = bytes[start..lim].iter().position(|&b| b == b'\n') {
127 return (start + i, start + i + 1);
128 }
129 if lim == len {
130 return (len, len);
131 }
132 let mut cut = lim;
133 while !text.is_char_boundary(cut) {
134 cut -= 1;
135 }
136 let from = cut.saturating_sub(BREAK_LOOKBACK).max(start + 1);
139 if from < cut
140 && let Some(p) = bytes[from..cut].iter().rposition(|&b| b == b' ')
141 {
142 cut = from + p + 1;
143 }
144 (cut, cut)
145}
146
147fn next_unit(text: &str, start: usize, lines: bool) -> Option<usize> {
148 let (end, next) = unit_bounds(text, start, lines);
149 (next > end || next < text.len()).then_some(next)
150}
151
152fn line_start(text: &str, pos: usize, lines: bool) -> usize {
154 if !lines {
155 return 0;
156 }
157 text.as_bytes()[..pos]
158 .iter()
159 .rposition(|&b| b == b'\n')
160 .map_or(0, |i| i + 1)
161}
162
163fn unit_of(text: &str, pos: usize, lines: bool) -> usize {
165 let mut at = line_start(text, pos, lines);
166 while let Some(next) = next_unit(text, at, lines) {
167 if next > pos {
168 break;
169 }
170 at = next;
171 }
172 at
173}
174
175fn prev_unit(text: &str, start: usize, lines: bool) -> Option<usize> {
176 if start == 0 {
177 return None;
178 }
179 Some(unit_of(text, start - 1, lines))
180}
181
182fn last_unit(text: &str, lines: bool) -> usize {
183 let len = text.len();
184 if lines && text.ends_with('\n') {
185 return len;
186 }
187 unit_of(text, len.saturating_sub(1), lines)
188}
189
190fn breaks_after(c: char) -> bool {
192 c.is_whitespace() || matches!(c, '-' | '/' | '&' | '?' | ',' | ';' | '|')
193}
194
195struct Filler {
197 rows: Vec<Row>,
198 cur: String,
199 cur_w: usize,
200 cur_start: usize,
201 last_break: Option<(usize, usize, usize)>,
204 width: usize,
205 hard: bool,
206}
207
208impl Filler {
209 fn end_row(&mut self, end: usize) {
210 self.rows.push(Row {
211 text: std::mem::take(&mut self.cur),
212 start: self.cur_start,
213 end,
214 });
215 self.cur_w = 0;
216 self.cur_start = end;
217 self.last_break = None;
218 }
219
220 fn push(&mut self, piece: &str, w: usize, at: usize, after: usize, brk: bool) {
223 if w > 0 && self.cur_w + w > self.width && !self.cur.is_empty() {
224 if !self.hard && piece.chars().all(char::is_whitespace) {
225 self.cur.push_str(piece);
227 self.end_row(after);
228 return;
229 }
230 match self.last_break.take() {
231 Some((byte, cells, src)) if !self.hard && byte < self.cur.len() => {
232 let carry = self.cur.split_off(byte);
233 let carried = self.cur_w - cells;
234 self.end_row(src);
235 self.cur = carry;
236 self.cur_w = carried;
237 if self.cur_w + w > self.width && !self.cur.is_empty() {
238 self.end_row(at);
239 }
240 }
241 _ => self.end_row(at),
242 }
243 }
244 self.cur.push_str(piece);
245 self.cur_w += w;
246 if brk && !self.hard {
247 self.last_break = Some((self.cur.len(), self.cur_w, after));
248 }
249 }
250}
251
252pub fn wrap_unit(
256 text: &str,
257 start: usize,
258 end: usize,
259 form: TextForm,
260 wrap: Wrap,
261 width: usize,
262) -> Vec<Row> {
263 let g = crate::glyphs::get();
264 let unit = &text[start..end];
265 let mut fill = Filler {
266 rows: Vec::new(),
267 cur: String::new(),
268 cur_w: 0,
269 cur_start: start,
270 last_break: None,
271 width: width.max(1),
272 hard: wrap == Wrap::Hard
273 || (unit.len() > TOKEN_RUN && !unit.bytes().any(|b| b == b' ' || b == b'\t')),
274 };
275 if form == TextForm::Escaped && start == 0 {
276 fill.push("\"", 1, start, start, false);
277 }
278 let ends_line = end < text.len() && text.as_bytes()[end] == b'\n';
279 let mut col = 0usize;
280 let mut piece = String::new();
281 for (i, c) in unit.char_indices() {
282 let at = start + i;
283 let after = at + c.len_utf8();
284 piece.clear();
285 let w = match form {
286 TextForm::Raw => match c {
287 '\r' if ends_line && after == end => continue,
289 '\t' => {
290 let stop = 4 - col % 4;
291 piece.extend(std::iter::repeat_n(' ', stop));
292 stop
293 }
294 c if crate::exact::marked(c) => {
295 piece.push_str(g.control_mark);
296 crate::glyphs::display_width(g.control_mark)
297 }
298 c => {
299 piece.push(c);
300 c.width().unwrap_or(0)
301 }
302 },
303 TextForm::Escaped => {
304 crate::exact::escape_char(c, &mut piece);
305 crate::glyphs::display_width(&piece)
306 }
307 };
308 col += w;
309 fill.push(&piece, w, at, after, breaks_after(c));
310 }
311 if form == TextForm::Escaped && end == text.len() {
312 fill.push("\"", 1, end, end, false);
313 }
314 if !fill.cur.is_empty() || fill.rows.is_empty() {
315 fill.end_row(end);
316 }
317 fill.rows
318}
319
320pub fn hex_row(bytes: &[u8], offset: usize, per_line: usize) -> String {
322 use std::fmt::Write;
323 let chunk = &bytes[offset..bytes.len().min(offset + per_line)];
324 let mut line = format!("{offset:08x} ");
325 for b in chunk {
326 let _ = write!(line, " {b:02x}");
327 }
328 for _ in chunk.len()..per_line {
329 line.push_str(" ");
330 }
331 line.push_str(" ");
332 line.extend(chunk.iter().map(|&b| {
333 if (0x20..0x7f).contains(&b) {
334 b as char
335 } else {
336 '.'
337 }
338 }));
339 line
340}
341
342pub fn hex_per_line(width: usize) -> usize {
346 [32, 16, 8]
347 .into_iter()
348 .find(|n| width >= 8 + 1 + n * 3 + 2 + n)
349 .unwrap_or(4)
350}
351
352#[derive(Debug, Clone, Default)]
354pub struct Reader {
355 key: Option<(u64, usize, Wrap)>,
358 top: (usize, usize),
360 cache: HashMap<usize, Vec<Row>>,
361 formatted: usize,
363 line_at: Option<(usize, usize)>,
365 resume: Option<usize>,
368}
369
370impl Reader {
371 pub fn prepare(&mut self, id: u64, width: usize, wrap: Wrap) {
373 let key = (id, width.max(1), wrap);
374 match self.key {
375 Some(k) if k == key => {}
376 Some((same, _, _)) if same == id => {
377 if let Some(start) = self
380 .cache
381 .get(&self.top.0)
382 .and_then(|rows| rows.get(self.top.1))
383 .map(|r| r.start)
384 {
385 self.resume = Some(start);
386 self.top.1 = 0;
387 }
388 self.key = Some(key);
389 self.cache.clear();
390 }
391 _ => {
392 self.key = Some(key);
393 self.cache.clear();
394 self.top = (0, 0);
395 self.line_at = None;
396 self.resume = None;
397 }
398 }
399 }
400
401 fn width(&self) -> usize {
402 self.key.map_or(1, |k| k.1)
403 }
404
405 pub fn carry(&mut self, old: &Content, new: &Content) {
408 if let (Content::Hex { per_line: was, .. }, Content::Hex { per_line: now, .. }) = (old, new)
409 && was != now
410 {
411 self.top.1 = self.top.1 * was / now;
412 }
413 }
414
415 fn settle(&mut self, c: &Content) {
417 let Content::Text { text, form, .. } = c else {
418 return;
419 };
420 if let Some(pos) = self.resume.take() {
421 let unit = self.top.0;
422 self.top.1 = self
423 .rows_of(text, *form, unit)
424 .iter()
425 .rposition(|r| r.start <= pos)
426 .unwrap_or(0);
427 }
428 }
429
430 fn wrap(&self) -> Wrap {
431 self.key.map_or(Wrap::Word, |k| k.2)
432 }
433
434 pub fn take_formatted(&mut self) -> usize {
436 std::mem::take(&mut self.formatted)
437 }
438
439 fn rows_of(&mut self, text: &str, form: TextForm, unit: usize) -> &Vec<Row> {
440 if !self.cache.contains_key(&unit) {
441 if self.cache.len() >= CACHE_UNITS {
442 self.cache.clear();
443 }
444 let lines = form == TextForm::Raw;
445 let (end, _) = unit_bounds(text, unit, lines);
446 self.formatted += end - unit;
447 let rows = wrap_unit(text, unit, end, form, self.wrap(), self.width());
448 self.cache.insert(unit, rows);
449 }
450 &self.cache[&unit]
451 }
452
453 fn count_rows(&mut self, text: &str, form: TextForm, unit: usize) -> usize {
454 self.rows_of(text, form, unit).len()
455 }
456
457 fn total(c: &Content) -> usize {
459 match c {
460 Content::Lines(lines) => lines.len(),
461 Content::Hex { bytes, per_line } => bytes.len().div_ceil(*per_line).max(1),
462 Content::Text { .. } => 0,
463 }
464 }
465
466 pub fn home(&mut self) {
467 self.top = (0, 0);
468 self.resume = None;
469 }
470
471 pub fn end(&mut self, c: &Content, h: usize) {
473 self.resume = None;
474 match c {
475 Content::Text { text, form, .. } => {
476 let last = last_unit(text, *form == TextForm::Raw);
477 let rows = self.count_rows(text, *form, last);
478 self.top = (last, rows);
479 self.up(c, h);
480 }
481 _ => self.top = (0, Self::total(c).saturating_sub(h)),
482 }
483 }
484
485 fn up(&mut self, c: &Content, mut n: usize) {
486 let Content::Text { text, form, .. } = c else {
487 self.top.1 = self.top.1.saturating_sub(n);
488 return;
489 };
490 let lines = *form == TextForm::Raw;
491 while n > 0 {
492 if self.top.1 > 0 {
493 let k = self.top.1.min(n);
494 self.top.1 -= k;
495 n -= k;
496 } else if let Some(prev) = prev_unit(text, self.top.0, lines) {
497 let rows = self.count_rows(text, *form, prev);
498 self.top = (prev, rows);
499 } else {
500 break;
501 }
502 }
503 }
504
505 fn down(&mut self, c: &Content, mut n: usize) {
506 let Content::Text { text, form, .. } = c else {
507 self.top.1 += n;
508 return;
509 };
510 let lines = *form == TextForm::Raw;
511 while n > 0 {
512 let rows = self.count_rows(text, *form, self.top.0);
513 if self.top.1 + n < rows {
514 self.top.1 += n;
515 break;
516 }
517 n -= rows - self.top.1;
518 match next_unit(text, self.top.0, lines) {
519 Some(next) => self.top = (next, 0),
520 None => {
521 self.top.1 = rows.saturating_sub(1);
522 break;
523 }
524 }
525 }
526 }
527
528 pub fn scroll(&mut self, c: &Content, h: usize, delta: isize) {
531 self.settle(c);
532 if delta < 0 {
533 self.up(c, delta.unsigned_abs());
534 } else {
535 self.down(c, delta as usize);
536 }
537 self.clamp(c, h);
538 }
539
540 fn clamp(&mut self, c: &Content, h: usize) {
542 match c {
543 Content::Text { .. } => {
544 let shown = self.window(c, h).rows.len();
545 if shown < h {
546 self.up(c, h - shown);
547 }
548 }
549 _ => self.top.1 = self.top.1.min(Self::total(c).saturating_sub(h)),
550 }
551 }
552
553 pub fn jump(&mut self, c: &Content, h: usize, pos: usize) {
555 self.resume = None;
556 match c {
557 Content::Text { text, form, .. } => {
558 let pos = pos.min(text.len());
559 let unit = unit_of(text, pos, *form == TextForm::Raw);
560 let row = self
561 .rows_of(text, *form, unit)
562 .iter()
563 .rposition(|r| r.start <= pos)
564 .unwrap_or(0);
565 self.top = (unit, row);
566 }
567 Content::Hex { per_line, .. } => self.top = (0, pos / per_line),
568 Content::Lines(_) => self.top = (0, pos),
569 }
570 self.up(c, (h / 4).min(2));
571 self.clamp(c, h);
572 }
573
574 pub fn window(&mut self, c: &Content, h: usize) -> Window {
576 self.settle(c);
577 let mut win = Window::default();
578 match c {
579 Content::Lines(lines) => {
580 let start = self.top.1.min(lines.len());
581 win.rows = lines[start..]
582 .iter()
583 .take(h)
584 .map(|(text, tone)| Shown {
585 text: text.clone(),
586 tone: *tone,
587 })
588 .collect();
589 win.above = start > 0;
590 win.below = start + h < lines.len();
591 win.from = start;
592 win.to = start + win.rows.len();
593 }
594 Content::Hex { bytes, per_line } => {
595 let total = Self::total(c);
596 let start = self.top.1.min(total.saturating_sub(1));
597 for i in start..total.min(start + h) {
598 win.rows.push(Shown {
599 text: hex_row(bytes, i * per_line, *per_line),
600 tone: Tone::Plain,
601 });
602 }
603 win.above = start > 0;
604 win.below = start + h < total;
605 win.from = start * per_line;
606 win.to = ((start + win.rows.len()) * per_line).min(bytes.len());
607 }
608 Content::Text { text, form, .. } => {
609 let lines = *form == TextForm::Raw;
610 let (mut unit, mut skip) = self.top;
611 win.above = unit > 0 || skip > 0;
612 win.from = unit;
613 'units: loop {
614 let rows = self.rows_of(text, *form, unit).clone();
615 for row in rows.into_iter().skip(skip) {
616 if win.rows.len() == h {
617 win.below = true;
618 break 'units;
619 }
620 if win.rows.is_empty() {
621 win.from = row.start;
622 }
623 win.to = row.end;
624 win.rows.push(Shown {
625 text: row.text,
626 tone: Tone::Plain,
627 });
628 }
629 skip = 0;
630 match next_unit(text, unit, lines) {
631 Some(next) => unit = next,
632 None => break,
633 }
634 }
635 }
636 }
637 win
638 }
639
640 pub fn rows_needed(&mut self, c: &Content, cap: usize) -> usize {
642 match c {
643 Content::Text { text, form, .. } => {
644 let lines = *form == TextForm::Raw;
645 let mut unit = 0;
646 let mut n = 0;
647 loop {
648 n += self.count_rows(text, *form, unit);
649 if n >= cap {
650 return cap;
651 }
652 match next_unit(text, unit, lines) {
653 Some(next) => unit = next,
654 None => return n,
655 }
656 }
657 }
658 _ => Self::total(c).min(cap),
659 }
660 }
661
662 fn line_of(&mut self, text: &str, pos: usize) -> usize {
664 let bytes = text.as_bytes();
665 let line = match self.line_at {
666 Some((at, line)) if at <= pos => line + bytecount(&bytes[at..pos], b'\n'),
667 Some((at, line)) if at - pos < pos => line - bytecount(&bytes[pos..at], b'\n'),
668 _ => 1 + bytecount(&bytes[..pos], b'\n'),
669 };
670 self.line_at = Some((pos, line));
671 line
672 }
673
674 pub fn position_width(c: &Content) -> usize {
677 let m = crate::glyphs::get().middot;
678 let widest = match c {
679 Content::Lines(lines) => {
680 let n = thousands(lines.len());
681 format!("lines {n}-{n} of {n}")
682 }
683 Content::Hex { bytes, .. } => {
684 let n = format!("0x{:x}", bytes.len());
685 format!("{n}-{n} of {n}")
686 }
687 Content::Text { lines, .. } if *lines > 1 => {
688 let n = thousands(*lines);
689 format!("lines {n}-{n} of {n} {m} 100%")
690 }
691 Content::Text { .. } => "100%".to_string(),
692 };
693 crate::glyphs::cell_width(&widest)
694 }
695
696 pub fn position(&mut self, c: &Content, win: &Window) -> String {
699 let m = crate::glyphs::get().middot;
700 match c {
701 Content::Lines(lines) => format!(
702 "lines {}-{} of {}",
703 thousands(win.from + 1),
704 thousands(win.to),
705 thousands(lines.len())
706 ),
707 Content::Hex { bytes, .. } => format!(
708 "0x{:x}-0x{:x} of 0x{:x}",
709 win.from,
710 win.to.saturating_sub(1),
711 bytes.len()
712 ),
713 Content::Text { text, lines, .. } => {
714 let percent = if text.is_empty() {
715 100
716 } else {
717 win.to * 100 / text.len()
718 };
719 if *lines > 1 {
720 let first = self.line_of(text, win.from);
721 let last = first + bytecount(&text.as_bytes()[win.from..win.to], b'\n');
722 format!(
723 "lines {}-{} of {} {m} {percent}%",
724 thousands(first),
725 thousands(last.min(*lines)),
726 thousands(*lines)
727 )
728 } else {
729 format!("{percent}%")
730 }
731 }
732 }
733 }
734}
735
736pub fn ignores_case(needle: &str) -> bool {
738 !needle.chars().any(char::is_uppercase)
739}
740
741pub fn find_hits(c: &Content, needle: &str) -> Vec<usize> {
744 if needle.is_empty() {
745 return Vec::new();
746 }
747 let fold = ignores_case(needle);
748 let needle_cmp = if fold {
749 needle.to_ascii_lowercase()
750 } else {
751 needle.to_string()
752 };
753 let search = |hay: &[u8]| -> Vec<usize> {
754 let n = needle_cmp.as_bytes();
755 if n.len() > hay.len() {
756 return Vec::new();
757 }
758 let first = n[0];
759 let mut hits = Vec::new();
760 let mut i = 0;
761 while i + n.len() <= hay.len() && hits.len() < MAX_HITS {
762 let b = if fold {
763 hay[i].to_ascii_lowercase()
764 } else {
765 hay[i]
766 };
767 if b == first {
768 let matched = hay[i..i + n.len()].iter().zip(n).all(|(&h, &w)| {
769 if fold {
770 h.to_ascii_lowercase() == w
771 } else {
772 h == w
773 }
774 });
775 if matched {
776 hits.push(i);
777 i += n.len();
778 continue;
779 }
780 }
781 i += 1;
782 }
783 hits
784 };
785 match c {
786 Content::Text { text, .. } => search(text.as_bytes()),
787 Content::Hex { bytes, .. } => search(bytes),
788 Content::Lines(lines) => lines
789 .iter()
790 .enumerate()
791 .filter(|(_, (text, _))| !search(text.as_bytes()).is_empty())
792 .map(|(i, _)| i)
793 .take(MAX_HITS)
794 .collect(),
795 }
796}
797
798pub fn hits_in_row(row: &str, needle: &str) -> Vec<(usize, usize)> {
800 if needle.is_empty() {
801 return Vec::new();
802 }
803 let fold = ignores_case(needle);
804 let (hay, n) = if fold {
805 (row.to_ascii_lowercase(), needle.to_ascii_lowercase())
806 } else {
807 (row.to_string(), needle.to_string())
808 };
809 hay.match_indices(&n)
810 .map(|(i, m)| (i, i + m.len()))
811 .filter(|(a, b)| row.is_char_boundary(*a) && row.is_char_boundary(*b))
812 .collect()
813}
814
815pub fn wrap_lines(text: &str, width: usize, tone: Tone, out: &mut Vec<(String, Tone)>) {
818 let mut at = 0;
819 for line in text.split('\n') {
820 let end = at + line.len();
821 for row in wrap_unit(text, at, end, TextForm::Raw, Wrap::Word, width) {
822 out.push((row.text, tone));
823 }
824 at = end + 1;
825 }
826}
827
828#[cfg(test)]
829mod tests {
830 use super::*;
831
832 fn rows(text: &str, width: usize, wrap: Wrap) -> Vec<String> {
833 wrap_unit(text, 0, text.len(), TextForm::Raw, wrap, width)
834 .into_iter()
835 .map(|r| r.text)
836 .collect()
837 }
838
839 #[test]
842 fn word_wrap_breaks_between_words_and_after_url_separators() {
843 let url = "https://shop.example.com/orders/segment0/segment1/segment2/segment3/segment4/segment5/segment6/segment7/segment8/segment9/segment10/segment11?id=0&utm_source=newsletter&utm_campaign=spring";
844 let out = rows(url, 76, Wrap::Word);
845 assert_eq!(out.concat(), url, "rows put back together are the value");
846 for r in &out {
847 assert!(crate::glyphs::display_width(r) <= 76, "{r}");
848 }
849 assert!(
850 out.iter().all(|r| !r.ends_with("utm_s")),
851 "no word split: {out:?}"
852 );
853 for r in &out[..out.len() - 1] {
854 assert!(
855 r.ends_with('/') || r.ends_with('&') || r.ends_with('?'),
856 "{out:?}"
857 );
858 }
859
860 let log = "2024-03-02T10:00:01Z INFO request_id=000001 path=/api/v1/orders/1 status=200 latency_ms=1 user_agent=Mozilla/5.0";
861 let out = rows(log, 60, Wrap::Word);
862 assert_eq!(out.concat(), log);
863 let words: Vec<&str> = log.split(' ').collect();
864 for r in &out {
865 let first = r.split(' ').next().unwrap();
867 assert!(
868 words.iter().any(|w| w.starts_with(first))
869 || words.iter().any(|w| w.contains(&format!("/{first}"))),
870 "{first:?} in {out:?}"
871 );
872 assert!(!r.starts_with("ms="), "{out:?}");
873 }
874 let hard = rows(log, 60, Wrap::Hard);
876 assert_eq!(hard[0].len(), 60);
877 }
878
879 #[test]
880 fn a_word_wider_than_the_row_is_cut_and_wide_characters_stay_whole() {
881 assert_eq!(rows("abcdefghij", 4, Wrap::Word), ["abcd", "efgh", "ij"]);
882 assert_eq!(
883 rows("東京大阪京都", 5, Wrap::Word),
884 ["東京", "大阪", "京都"]
885 );
886 let b64 = "AbC/".repeat(200);
888 let out = rows(&b64, 50, Wrap::Word);
889 assert!(out[..out.len() - 1].iter().all(|r| r.len() == 50));
890 }
891
892 #[test]
893 fn tabs_space_to_stops_and_controls_are_marked() {
894 let g = crate::glyphs::get();
895 assert_eq!(rows("a\tb", 40, Wrap::Word), ["a b"]);
896 assert_eq!(
897 rows("x\u{7}y", 40, Wrap::Word),
898 [format!("x{}y", g.control_mark)]
899 );
900 let esc = wrap_unit("a\nb", 0, 3, TextForm::Escaped, Wrap::Word, 40);
901 assert_eq!(esc[0].text, r#""a\nb""#);
902 }
903
904 #[test]
906 fn units_are_found_alike_from_either_direction() {
907 let text: String = (0..300)
908 .map(|i| format!("line {i} {}\n", "word ".repeat(i % 7)))
909 .collect::<String>()
910 + &"x".repeat(SEG * 3 + 17)
911 + "\ntail";
912 let mut down = vec![0];
913 while let Some(n) = next_unit(&text, *down.last().unwrap(), true) {
914 down.push(n);
915 }
916 let mut up = vec![last_unit(&text, true)];
917 while let Some(p) = prev_unit(&text, *up.last().unwrap(), true) {
918 up.push(p);
919 }
920 up.reverse();
921 assert_eq!(down, up);
922 assert_eq!(last_unit("a\n", true), 2);
924 assert_eq!(next_unit("a\n", 0, true), Some(2));
925 assert_eq!(next_unit("a\n", 2, true), None);
926 }
927
928 fn reader(text: &str, width: usize) -> (Reader, Content) {
929 let mut r = Reader::default();
930 r.prepare(1, width, Wrap::Word);
931 (r, Content::text(Arc::from(text), TextForm::Raw))
932 }
933
934 #[test]
937 fn end_and_home_wrap_only_what_is_on_screen() {
938 let prose: String = (0..30_000)
939 .map(|i| format!("paragraph {i} of words that wrap at the pane edge.\n"))
940 .collect::<String>()
941 + &"y".repeat(2 << 20);
942 let (mut r, c) = reader(&prose, 100);
943 let first = r.window(&c, 40);
944 assert_eq!(
945 first.rows[0].text,
946 "paragraph 0 of words that wrap at the pane edge."
947 );
948 assert!(r.take_formatted() <= crate::inspector::inspector_modal::CHUNK_BYTES);
949 r.end(&c, 40);
950 let last = r.window(&c, 40);
951 assert!(!last.below && last.above);
952 assert_eq!(last.rows.len(), 40);
953 assert!(last.rows.iter().all(|row| row.text.starts_with('y')));
954 assert_eq!(last.to, prose.len());
955 assert!(r.take_formatted() <= crate::inspector::inspector_modal::CHUNK_BYTES);
956 for _ in 0..50 {
957 r.scroll(&c, 40, -39);
958 r.window(&c, 40);
959 assert!(r.take_formatted() <= crate::inspector::inspector_modal::CHUNK_BYTES);
960 }
961 r.home();
962 assert_eq!(r.window(&c, 40).from, 0);
963 }
964
965 #[test]
966 fn scrolling_stops_with_the_last_row_at_the_bottom() {
967 let text = (1..=10)
968 .map(|i| format!("{i}"))
969 .collect::<Vec<_>>()
970 .join("\n");
971 let (mut r, c) = reader(&text, 20);
972 r.scroll(&c, 4, 100);
973 let w = r.window(&c, 4);
974 let shown: Vec<&str> = w.rows.iter().map(|s| s.text.as_str()).collect();
975 assert_eq!(shown, ["7", "8", "9", "10"]);
976 assert_eq!(
977 r.position(&c, &w),
978 format!("lines 7-10 of 10 {} 100%", crate::glyphs::get().middot)
979 );
980 r.scroll(&c, 4, -2);
981 assert_eq!(r.window(&c, 4).rows[0].text, "5");
982 let (mut r, c) = reader("a\nb", 20);
984 r.scroll(&c, 4, 3);
985 assert_eq!(r.window(&c, 4).rows.len(), 2);
986 }
987
988 #[test]
989 fn a_search_finds_every_place_and_jumps_there() {
990 let text: String = (0..4000)
991 .map(|i| format!("row {i} status=200\n"))
992 .collect::<String>()
993 + "row 4000 status=500";
994 let c = Content::text(Arc::from(text.as_str()), TextForm::Raw);
995 assert!(
996 find_hits(&c, "STATUS=500").is_empty(),
997 "capitals match case"
998 );
999 let hits = find_hits(&c, "status=500");
1000 assert_eq!(hits, vec![text.find("status=500").unwrap()]);
1001 let mut r = Reader::default();
1002 r.prepare(1, 80, Wrap::Word);
1003 r.jump(&c, 10, hits[0]);
1004 let w = r.window(&c, 10);
1005 assert!(w.rows.iter().any(|row| row.text.contains("status=500")));
1006 assert_eq!(
1007 hits_in_row("a Status x status", "status"),
1008 [(2, 8), (11, 17)]
1009 );
1010 }
1011
1012 #[test]
1015 fn a_rewrap_keeps_the_place_in_the_value() {
1016 let text: String = (0..200)
1017 .map(|i| format!("line {i} {}\n", "word ".repeat(30)))
1018 .collect();
1019 let (mut r, c) = reader(&text, 60);
1020 r.scroll(&c, 10, 75);
1021 let pos = r.window(&c, 10).from;
1022 assert!(pos > 0);
1023 r.prepare(1, 100, Wrap::Word);
1024 let from = r.window(&c, 10).from;
1025 assert!(from <= pos && pos - from < 100, "{from} for {pos}");
1026 r.prepare(1, 40, Wrap::Hard);
1027 let from = r.window(&c, 10).from;
1028 assert!(from <= pos && pos - from < 40, "{from} for {pos}");
1029
1030 let bytes: Arc<[u8]> = Arc::from(vec![0u8; 4096]);
1031 let narrow = Content::Hex {
1032 bytes: bytes.clone(),
1033 per_line: 8,
1034 };
1035 let wide = Content::Hex {
1036 bytes,
1037 per_line: 32,
1038 };
1039 let mut r = Reader::default();
1040 r.prepare(2, 50, Wrap::Word);
1041 r.scroll(&narrow, 10, 100);
1042 assert_eq!(r.window(&narrow, 10).from, 800);
1043 r.prepare(2, 140, Wrap::Word);
1044 r.carry(&narrow, &wide);
1045 assert_eq!(r.window(&wide, 10).from, 800);
1046 }
1047
1048 #[test]
1049 fn hex_rows_are_found_by_offset() {
1050 let bytes: Arc<[u8]> = Arc::from(vec![0x41u8; 1 << 20]);
1051 let c = Content::Hex {
1052 bytes,
1053 per_line: 16,
1054 };
1055 let mut r = Reader::default();
1056 r.prepare(1, 80, Wrap::Word);
1057 r.end(&c, 20);
1058 let w = r.window(&c, 20);
1059 assert!(w.rows.last().unwrap().text.starts_with("000ffff0"));
1060 assert_eq!(r.position(&c, &w), "0xffec0-0xfffff of 0x100000");
1061 assert_eq!(hex_per_line(150), 32);
1062 assert_eq!(hex_per_line(100), 16);
1063 }
1064}