#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Mark {
Plain,
Semicolon,
OpenParen,
CloseParen,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Edit {
pub start: u32,
pub end: u32,
pub mark: Mark,
}
const fn is_line_terminator(c: char) -> bool {
matches!(c, '\n' | '\r' | '\u{2028}' | '\u{2029}')
}
pub(super) fn apply(source: &str, edits: &mut [Edit]) -> String {
edits.sort_by_key(|edit| edit.start);
let mut out = String::with_capacity(source.len());
let mut copied = 0usize;
for edit in edits.iter() {
let end = edit.end as usize;
if end <= copied {
continue;
}
let start = (edit.start as usize).max(copied);
let mark = match edit.mark {
Mark::Semicolon | Mark::OpenParen if start != edit.start as usize => Mark::Plain,
mark => mark,
};
out.push_str(&source[copied..start]);
blank_into(&mut out, &source[start..end], mark);
copied = end;
}
out.push_str(&source[copied..]);
out
}
fn blank_into(out: &mut String, text: &str, mark: Mark) {
let mut chars = text.chars();
let lead = match mark {
Mark::Semicolon => Some(';'),
Mark::OpenParen => Some('('),
Mark::Plain | Mark::CloseParen => None,
};
if let Some(lead) = lead
&& let Some(first) = chars.next()
{
if is_line_terminator(first) {
debug_assert!(false, "a substitution anchored on a line terminator");
out.push(first);
} else {
out.push(lead);
push_spaces(out, first.len_utf16() - 1);
}
}
let last = if mark == Mark::CloseParen {
chars.next_back()
} else {
None
};
blank_text_into(out, chars.as_str());
if let Some(last) = last {
if is_line_terminator(last) {
debug_assert!(false, "a substitution anchored on a line terminator");
out.push(last);
} else {
push_spaces(out, last.len_utf16() - 1);
out.push(')');
}
}
}
fn blank_text_into(out: &mut String, text: &str) {
let bytes = text.as_bytes();
let mut units = 0;
let mut at = 0;
loop {
while let Some(word) = bytes[at..].first_chunk::<8>() {
if word_needs_a_closer_look(u64::from_ne_bytes(*word)) {
break;
}
units += 8;
at += 8;
}
let Some(&byte) = bytes.get(at) else {
break;
};
let terminator_len = match byte {
b'\n' | b'\r' => 1,
0xE2 if bytes.get(at + 1) == Some(&0x80)
&& matches!(bytes.get(at + 2), Some(0xA8 | 0xA9)) =>
{
3
}
_ => 0,
};
if terminator_len == 0 {
units += usize::from(byte & 0xC0 != 0x80) + usize::from(byte >= 0xF0);
at += 1;
} else {
push_spaces(out, units);
units = 0;
out.push_str(&text[at..at + terminator_len]);
at += terminator_len;
}
}
push_spaces(out, units);
}
const fn word_needs_a_closer_look(word: u64) -> bool {
const ONES: u64 = u64::from_ne_bytes([0x01; 8]);
const HIGHS: u64 = u64::from_ne_bytes([0x80; 8]);
let lf = word ^ (ONES * b'\n' as u64);
let cr = word ^ (ONES * b'\r' as u64);
((lf.wrapping_sub(ONES) & !lf) | (cr.wrapping_sub(ONES) & !cr) | word) & HIGHS != 0
}
const SPACES: &str = " ";
fn push_spaces(out: &mut String, mut count: usize) {
while count > SPACES.len() {
out.push_str(SPACES);
count -= SPACES.len();
}
out.push_str(&SPACES[..count]);
}
#[cfg(test)]
mod tests {
use super::{Edit, Mark, apply, blank_text_into, is_line_terminator};
fn run(source: &str, edits: &[(u32, u32, Mark)]) -> String {
let mut edits: Vec<Edit> = edits
.iter()
.map(|&(start, end, mark)| Edit { start, end, mark })
.collect();
apply(source, &mut edits)
}
#[test]
fn blanks_per_utf16_unit_and_keeps_terminators() {
let source = "a:é😀\r\n\u{2028}\u{2029}b";
let out = run(source, &[(1, source.len() as u32 - 1, Mark::Plain)]);
assert_eq!(out, "a \r\n\u{2028}\u{2029}b");
}
#[test]
fn writes_each_substitution() {
assert_eq!(run("x as T", &[(1, 6, Mark::Semicolon)]), "x; ");
assert_eq!(
run("<T>(a)", &[(0, 3, Mark::OpenParen), (3, 4, Mark::Plain)]),
"( a)"
);
assert_eq!(run("(a): U", &[(2, 6, Mark::CloseParen)]), "(a )");
}
#[test]
fn close_paren_takes_the_last_unit_of_an_astral_character() {
assert_eq!(run("a: 😀", &[(1, 7, Mark::CloseParen)]), "a )");
}
#[test]
fn sorts_and_clips_overlaps() {
assert_eq!(
run(
"abcdef",
&[
(3, 5, Mark::Plain),
(0, 2, Mark::Plain),
(1, 4, Mark::Semicolon)
]
),
" f"
);
}
fn blank_by_chars(text: &str) -> String {
let mut out = String::new();
for c in text.chars() {
if is_line_terminator(c) {
out.push(c);
} else {
out.extend(std::iter::repeat_n(' ', c.len_utf16()));
}
}
out
}
#[test]
fn blanks_by_bytes_as_by_characters() {
let specials = [
"\n", "\r", "\r\n", "\u{2028}", "\u{2029}", "\u{2027}", "\u{202A}", "\u{20a9}", "é",
"\u{a0}", "中", "\u{feff}", "😀", "\t", "x",
];
for special in specials {
for second in specials {
for pad in 0..20 {
for tail in [0, 7, 70, 140] {
let text = format!(
"{}{special}{}{second}{}",
"a".repeat(pad),
"b".repeat(pad % 9),
"c".repeat(tail)
);
let mut out = String::new();
blank_text_into(&mut out, &text);
assert_eq!(out, blank_by_chars(&text), "{text:?}");
}
}
}
}
}
}