Skip to main content

diffs_rs/
text.rs

1use std::fmt::{self, Write};
2
3use unicode_segmentation::UnicodeSegmentation;
4use unicode_width::UnicodeWidthStr;
5
6mod offsets;
7pub(crate) use offsets::source_offsets;
8
9pub fn safe_text(bytes: &[u8]) -> String {
10    let mut result = String::new();
11    append_bytes(&mut result, bytes, true).expect("Writing to a String cannot fail");
12    result
13}
14
15fn append_bytes(out: &mut impl Write, bytes: &[u8], complete: bool) -> Result<usize, fmt::Error> {
16    let mut rest = bytes;
17    while !rest.is_empty() {
18        match std::str::from_utf8(rest) {
19            Ok(text) => {
20                append_safe(out, text)?;
21                return Ok(bytes.len());
22            }
23            Err(error) => {
24                let n = error.valid_up_to();
25                append_safe(
26                    out,
27                    std::str::from_utf8(&rest[..n]).expect("valid UTF-8 prefix"),
28                )?;
29                if !complete && error.error_len().is_none() {
30                    return Ok(bytes.len() - rest.len() + n);
31                }
32                let end = n + error.error_len().unwrap_or(rest.len() - n);
33                for b in &rest[n..end] {
34                    write!(out, "\\x{b:02x}")?;
35                }
36                rest = &rest[end..];
37            }
38        }
39    }
40    Ok(bytes.len())
41}
42
43fn append_safe(out: &mut impl Write, text: &str) -> fmt::Result {
44    for c in text.chars() {
45        if c == '\n' || c == '\t' {
46            out.write_char(c)?;
47        } else if c.is_control() || matches!(c, '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}') {
48            write!(out, "\\u{{{:x}}}", c as u32)?;
49        } else {
50            out.write_char(c)?;
51        }
52    }
53    Ok(())
54}
55
56pub fn display_text(bytes: &[u8]) -> String {
57    let bytes = if let Some(line) = bytes.strip_suffix(b"\n") {
58        line.strip_suffix(b"\r").unwrap_or(line)
59    } else {
60        bytes
61    };
62    safe_text(bytes)
63}
64
65pub fn safe_label(bytes: &[u8]) -> String {
66    safe_text(bytes).replace('\n', "\\n").replace('\t', "\\t")
67}
68
69pub struct Glyph<'a> {
70    pub text: &'a str,
71    pub byte: usize,
72    pub column: usize,
73    pub width: usize,
74}
75
76pub fn glyphs(text: &str, tab_width: usize) -> impl Iterator<Item = Glyph<'_>> {
77    glyphs_from(text, tab_width, 0, 0)
78}
79
80pub fn glyphs_from(
81    text: &str,
82    tab_width: usize,
83    start_byte: usize,
84    mut column: usize,
85) -> impl Iterator<Item = Glyph<'_>> {
86    text[start_byte..]
87        .grapheme_indices(true)
88        .map(move |(byte, text)| {
89            let width = if text == "\t" {
90                tab_width - column % tab_width
91            } else {
92                text.width().max(1)
93            };
94            let glyph = Glyph {
95                text,
96                byte: start_byte + byte,
97                column,
98                width,
99            };
100            column += width;
101            glyph
102        })
103}