Skip to main content

datui_lib/inspector/
inspector_reader.rs

1//! The inspector's value reader: a long value read a screen at a time.
2//!
3//! Text is cut into units — a line, or a segment of [`SEG`] bytes of a longer
4//! line — and only the units on screen are wrapped. Scrolling walks units from
5//! the top of the pane; End finds the last unit by searching back for a line
6//! break and fills the pane upward. So the end of a 2 MiB value is as near as
7//! its start, and no key wraps more than a screen and a unit of it.
8//!
9//! A unit's bounds depend only on the text, never on how it was reached, so a
10//! unit wrapped walking down is the unit found walking up.
11
12use crate::app::modals::copy_modal::thousands;
13use std::collections::HashMap;
14use std::sync::Arc;
15use unicode_width::UnicodeWidthChar;
16
17/// The most bytes of a long line wrapped as one unit.
18pub const SEG: usize = 2048;
19/// How far back from a segment's end a space is looked for, to end it there.
20const BREAK_LOOKBACK: usize = 256;
21/// A unit with no space and longer than this is a token run (base64, hex):
22/// word wrap would cut it at its few slashes, so it fills each row instead.
23const TOKEN_RUN: usize = 512;
24/// Units kept wrapped; past this the cache starts over.
25const CACHE_UNITS: usize = 256;
26/// The most matches a search in a value lists.
27pub 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/// How a line wider than the pane breaks: at spaces and after `/ & ? , ; | -`,
37/// or at the pane's edge.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum Wrap {
40    #[default]
41    Word,
42    Hard,
43}
44
45/// Text as itself (lines broken, tabs spaced, controls marked) or as an escaped
46/// literal on one line.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum TextForm {
49    Raw,
50    Escaped,
51}
52
53/// What the value pane reads.
54#[derive(Debug, Clone)]
55pub enum Content {
56    /// A few lines, wrapped whole: a scalar, a null, a message.
57    Lines(Vec<(String, Tone)>),
58    /// Text of any length, wrapped as it comes on screen. `lines` is its count
59    /// of lines, counted once.
60    Text {
61        text: Arc<str>,
62        form: TextForm,
63        lines: usize,
64    },
65    /// Bytes as a hex dump, `per_line` to a row: any row is found by its offset.
66    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    /// Whether `w` changes how this reads: only text wraps.
79    pub fn wraps(&self) -> bool {
80        matches!(self, Content::Text { .. })
81    }
82}
83
84/// Lines in `text`: its line breaks, plus one.
85pub 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/// One row of wrapped text, and the bytes of the value it shows.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct Row {
96    pub text: String,
97    pub start: usize,
98    pub end: usize,
99}
100
101/// A row on screen.
102#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct Shown {
104    pub text: String,
105    pub tone: Tone,
106}
107
108/// The rows on screen, and whether any are above or below them.
109#[derive(Debug, Clone, Default)]
110pub struct Window {
111    pub rows: Vec<Shown>,
112    pub above: bool,
113    pub below: bool,
114    /// Where the window starts and ends in the value: bytes for text and hex,
115    /// rows for lines.
116    pub from: usize,
117    pub to: usize,
118}
119
120/// Where a unit of text ends, and where the next starts: past its line break,
121/// or at its end when the line is cut into segments.
122fn 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    // End the segment after a space where there is one near: a row then breaks
137    // where it would have anyway.
138    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
152/// Where the line holding byte `pos` starts.
153fn 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
163/// The unit holding byte `pos`: found from its line's start, as walking down finds it.
164fn 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
190/// Whether a row may end after `c` under word wrap.
191fn breaks_after(c: char) -> bool {
192    c.is_whitespace() || matches!(c, '-' | '/' | '&' | '?' | ',' | ';' | '|')
193}
194
195/// Rows being filled, a piece of text at a time.
196struct Filler {
197    rows: Vec<Row>,
198    cur: String,
199    cur_w: usize,
200    cur_start: usize,
201    /// Where the row may end under word wrap: bytes into `cur`, its cells, and
202    /// the value's byte after it.
203    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    /// Add `piece`, `w` cells wide, for the value's bytes `at..after`; `brk` when a
221    /// row may end after it.
222    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                // A space at the edge hangs past it: the row ends after it.
226                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
252/// Wrap bytes `start..end` of `text` to rows of at most `width` cells. Raw text
253/// spaces its tabs to stops of four and marks controls; escaped text is the
254/// literal, quoted at the value's two ends.
255pub 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                // A Windows line end is one break.
288                '\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
320/// A hex dump row: offset, the bytes, and their printable ASCII.
321pub 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
342/// Bytes to a hex row at `width` cells: 32 where a row of them fits, then 16,
343/// 8, 4. A row of `n` is the offset and a space, three cells a byte, two
344/// spaces, and a cell a byte.
345pub 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/// Where a pane is in its value, and the units of text wrapped so far.
353#[derive(Debug, Clone, Default)]
354pub struct Reader {
355    /// What the reader reads: the pane's identity, its width and wrap. A new one
356    /// starts at the top; the same one at another width keeps its unit.
357    key: Option<(u64, usize, Wrap)>,
358    /// The unit at the top (its first byte; 0 for lines and hex) and the row of it.
359    top: (usize, usize),
360    cache: HashMap<usize, Vec<Row>>,
361    /// Bytes of text wrapped since [`Reader::take_formatted`] last asked.
362    formatted: usize,
363    /// A byte, and the line it is on, so a line number is counted from near it.
364    line_at: Option<(usize, usize)>,
365    /// The first byte of the top row before a rewrap: the row that holds it goes
366    /// back to the top once the unit is wrapped again.
367    resume: Option<usize>,
368}
369
370impl Reader {
371    /// Read the pane `id` at `width`, wrapped as `wrap`.
372    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                // Rewrapped (a resize, or `w`): the top row's first byte stays at
378                // the top. Lines and hex rows keep their index; `carry` rescales hex.
379                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    /// The same value built again at another width: a hex dump's top row moves to
406    /// the row holding the same offset.
407    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    /// After a rewrap, find the row that holds the byte that was at the top.
416    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    /// Bytes wrapped since the last ask: what a key cost.
435    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    /// Rows of a lines or hex value.
458    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    /// The last rows of the value, filling a pane of `h` rows.
472    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    /// Move `delta` rows, never past the top or past where the last row is at the
529    /// bottom of a pane of `h` rows.
530    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    /// Keep the pane full: past the end, the top comes back up.
541    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    /// Put byte `pos` (a row, for lines) near the top of a pane of `h` rows.
554    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    /// The rows of a pane of `h` rows from the top.
575    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    /// Rows the value takes, counted up to `cap`: how much of the pane it needs.
641    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    /// The line byte `pos` of `text` is on, counted from the nearest line known.
663    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    /// The widest [`Self::position`] can be for `c`, so what sits beside it on the
675    /// rule does not move as the pane scrolls.
676    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    /// Where the pane is, for its rule: `lines 41-73 of 4,000 · 1%`, an offset
697    /// range for bytes.
698    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
736/// Whether a search for `needle` ignores case: only when it has no capitals.
737pub fn ignores_case(needle: &str) -> bool {
738    !needle.chars().any(char::is_uppercase)
739}
740
741/// Every place `needle` is in the value, up to [`MAX_HITS`]: bytes into text or
742/// hex, rows of lines.
743pub 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
798/// Where `needle` is in a row on screen, as byte ranges of its text.
799pub 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
815/// Lines of a short text, wrapped to `width` at word breaks, for the pane's own
816/// sentences and short values.
817pub 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    /// D7: word wrap never splits a word that fits a row; a URL breaks after its
840    /// slashes and ampersands.
841    #[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            // Every row starts at a word, or after a slash inside one.
866            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        // Hard wrap fills every row.
875        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        // A token run (base64) fills its rows rather than breaking at slashes.
887        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    /// Units walk the same way down and up, and End lands on the last rows.
905    #[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        // A trailing line break has its empty last line.
923        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    /// The end of a 2 MiB value is one key away, and no key wraps more than a
935    /// chunk of it: the pane's rows and a unit.
936    #[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        // A value shorter than the pane stays at its top.
983        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    /// A resize rewraps the value: the row at the top still holds the byte that
1013    /// was there, and a hex dump keeps its offset at another row length.
1014    #[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}