blob: 43758eea1616e1f2c79f553f35f25b259a6cf92f [file] [log] [blame]
use super::Data;
use super::Inline;
use super::Position;
pub(crate) fn get() -> std::sync::MutexGuard<'static, Runtime> {
static RT: std::sync::Mutex<Runtime> = std::sync::Mutex::new(Runtime::new());
RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
#[derive(Default)]
pub(crate) struct Runtime {
per_file: Vec<FileRuntime>,
}
impl Runtime {
const fn new() -> Self {
Self {
per_file: Vec::new(),
}
}
pub(crate) fn write(&mut self, actual: &Data, inline: &Inline) -> std::io::Result<()> {
let actual = actual.render().expect("`actual` must be UTF-8");
if let Some(entry) = self
.per_file
.iter_mut()
.find(|f| f.path == inline.position.file)
{
entry.update(&actual, inline)?;
} else {
let mut entry = FileRuntime::new(inline)?;
entry.update(&actual, inline)?;
self.per_file.push(entry);
}
Ok(())
}
}
struct FileRuntime {
path: std::path::PathBuf,
original_text: String,
patchwork: Patchwork,
}
impl FileRuntime {
fn new(inline: &Inline) -> std::io::Result<FileRuntime> {
let path = inline.position.file.clone();
let original_text = std::fs::read_to_string(&path)?;
let patchwork = Patchwork::new(original_text.clone());
Ok(FileRuntime {
path,
original_text,
patchwork,
})
}
fn update(&mut self, actual: &str, inline: &Inline) -> std::io::Result<()> {
let span = Span::from_pos(&inline.position, &self.original_text);
let desired_indent = if inline.indent {
Some(span.line_indent)
} else {
None
};
let patch = format_patch(desired_indent, actual);
self.patchwork.patch(span.literal_range, &patch);
std::fs::write(&inline.position.file, &self.patchwork.text)
}
}
#[derive(Debug)]
struct Patchwork {
text: String,
indels: Vec<(std::ops::Range<usize>, usize)>,
}
impl Patchwork {
fn new(text: String) -> Patchwork {
Patchwork {
text,
indels: Vec::new(),
}
}
fn patch(&mut self, mut range: std::ops::Range<usize>, patch: &str) {
self.indels.push((range.clone(), patch.len()));
self.indels.sort_by_key(|(delete, _insert)| delete.start);
let (delete, insert) = self
.indels
.iter()
.take_while(|(delete, _)| delete.start < range.start)
.map(|(delete, insert)| (delete.end - delete.start, insert))
.fold((0usize, 0usize), |(x1, y1), (x2, y2)| (x1 + x2, y1 + y2));
for pos in &mut [&mut range.start, &mut range.end] {
**pos -= delete;
**pos += insert;
}
self.text.replace_range(range, patch);
}
}
fn lit_kind_for_patch(patch: &str) -> StrLitKind {
let has_dquote = patch.chars().any(|c| c == '"');
if !has_dquote {
let has_bslash_or_newline = patch.chars().any(|c| matches!(c, '\\' | '\n'));
return if has_bslash_or_newline {
StrLitKind::Raw(1)
} else {
StrLitKind::Normal
};
}
// Find the maximum number of hashes that follow a double quote in the string.
// We need to use one more than that to delimit the string.
let leading_hashes = |s: &str| s.chars().take_while(|&c| c == '#').count();
let max_hashes = patch.split('"').map(leading_hashes).max().unwrap();
StrLitKind::Raw(max_hashes + 1)
}
fn format_patch(desired_indent: Option<usize>, patch: &str) -> String {
let lit_kind = lit_kind_for_patch(patch);
let indent = desired_indent.map(|it| " ".repeat(it));
let is_multiline = patch.contains('\n');
let mut buf = String::new();
if matches!(lit_kind, StrLitKind::Raw(_)) {
buf.push('[');
}
lit_kind.write_start(&mut buf).unwrap();
if is_multiline {
buf.push('\n');
}
let mut final_newline = false;
for line in crate::utils::LinesWithTerminator::new(patch) {
if is_multiline && !line.trim().is_empty() {
if let Some(indent) = &indent {
buf.push_str(indent);
buf.push_str(" ");
}
}
buf.push_str(line);
final_newline = line.ends_with('\n');
}
if final_newline {
if let Some(indent) = &indent {
buf.push_str(indent);
}
}
lit_kind.write_end(&mut buf).unwrap();
if matches!(lit_kind, StrLitKind::Raw(_)) {
buf.push(']');
}
buf
}
#[derive(Clone, Debug)]
struct Span {
line_indent: usize,
/// The byte range of the argument to `expect!`, including the inner `[]` if it exists.
literal_range: std::ops::Range<usize>,
}
impl Span {
fn from_pos(pos: &Position, file: &str) -> Span {
let mut target_line = None;
let mut line_start = 0;
for (i, line) in crate::utils::LinesWithTerminator::new(file).enumerate() {
if i == pos.line as usize - 1 {
// `column` points to the first character of the macro invocation:
//
// expect![[r#""#]] expect![""]
// ^ ^ ^ ^
// column offset offset
//
// Seek past the exclam, then skip any whitespace and
// the macro delimiter to get to our argument.
#[allow(clippy::skip_while_next)]
let byte_offset = line
.char_indices()
.skip((pos.column - 1).try_into().unwrap())
.skip_while(|&(_, c)| c != '!')
.skip(1) // !
.skip_while(|&(_, c)| c.is_whitespace())
.skip(1) // [({
.skip_while(|&(_, c)| c.is_whitespace())
.next()
.expect("Failed to parse macro invocation")
.0;
let literal_start = line_start + byte_offset;
let indent = line.chars().take_while(|&it| it == ' ').count();
target_line = Some((literal_start, indent));
break;
}
line_start += line.len();
}
let (literal_start, line_indent) = target_line.unwrap();
let lit_to_eof = &file[literal_start..];
let lit_to_eof_trimmed = lit_to_eof.trim_start();
let literal_start = literal_start + (lit_to_eof.len() - lit_to_eof_trimmed.len());
let literal_len =
locate_end(lit_to_eof_trimmed).expect("Couldn't find closing delimiter for `expect!`.");
let literal_range = literal_start..literal_start + literal_len;
Span {
line_indent,
literal_range,
}
}
}
fn locate_end(arg_start_to_eof: &str) -> Option<usize> {
match arg_start_to_eof.chars().next()? {
c if c.is_whitespace() => panic!("skip whitespace before calling `locate_end`"),
// expect![[]]
'[' => {
let str_start_to_eof = arg_start_to_eof[1..].trim_start();
let str_len = find_str_lit_len(str_start_to_eof)?;
let str_end_to_eof = &str_start_to_eof[str_len..];
let closing_brace_offset = str_end_to_eof.find(']')?;
Some((arg_start_to_eof.len() - str_end_to_eof.len()) + closing_brace_offset + 1)
}
// expect![] | expect!{} | expect!()
']' | '}' | ')' => Some(0),
// expect!["..."] | expect![r#"..."#]
_ => find_str_lit_len(arg_start_to_eof),
}
}
/// Parses a string literal, returning the byte index of its last character
/// (either a quote or a hash).
fn find_str_lit_len(str_lit_to_eof: &str) -> Option<usize> {
fn try_find_n_hashes(
s: &mut impl Iterator<Item = char>,
desired_hashes: usize,
) -> Option<(usize, Option<char>)> {
let mut n = 0;
loop {
match s.next()? {
'#' => n += 1,
c => return Some((n, Some(c))),
}
if n == desired_hashes {
return Some((n, None));
}
}
}
let mut s = str_lit_to_eof.chars();
let kind = match s.next()? {
'"' => StrLitKind::Normal,
'r' => {
let (n, c) = try_find_n_hashes(&mut s, usize::MAX)?;
if c != Some('"') {
return None;
}
StrLitKind::Raw(n)
}
_ => return None,
};
let mut oldc = None;
loop {
let c = oldc.take().or_else(|| s.next())?;
match (c, kind) {
('\\', StrLitKind::Normal) => {
let _escaped = s.next()?;
}
('"', StrLitKind::Normal) => break,
('"', StrLitKind::Raw(0)) => break,
('"', StrLitKind::Raw(n)) => {
let (seen, c) = try_find_n_hashes(&mut s, n)?;
if seen == n {
break;
}
oldc = c;
}
_ => {}
}
}
Some(str_lit_to_eof.len() - s.as_str().len())
}
#[derive(Copy, Clone)]
enum StrLitKind {
Normal,
Raw(usize),
}
impl StrLitKind {
fn write_start(self, w: &mut impl std::fmt::Write) -> std::fmt::Result {
match self {
Self::Normal => write!(w, "\""),
Self::Raw(n) => {
write!(w, "r")?;
for _ in 0..n {
write!(w, "#")?;
}
write!(w, "\"")
}
}
}
fn write_end(self, w: &mut impl std::fmt::Write) -> std::fmt::Result {
match self {
Self::Normal => write!(w, "\""),
Self::Raw(n) => {
write!(w, "\"")?;
for _ in 0..n {
write!(w, "#")?;
}
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assert_eq;
use crate::str;
use crate::ToDebug as _;
#[test]
fn test_format_patch() {
let patch = format_patch(None, "hello\nworld\n");
assert_eq(
str![[r##"
[r#"
hello
world
"#]"##]],
patch,
);
let patch = format_patch(None, r"hello\tworld");
assert_eq(str![[r##"[r#"hello\tworld"#]"##]], patch);
let patch = format_patch(None, "{\"foo\": 42}");
assert_eq(str![[r##"[r#"{"foo": 42}"#]"##]], patch);
let patch = format_patch(Some(0), "hello\nworld\n");
assert_eq(
str![[r##"
[r#"
hello
world
"#]"##]],
patch,
);
let patch = format_patch(Some(4), "single line");
assert_eq(str![[r#""single line""#]], patch);
}
#[test]
fn test_patchwork() {
let mut patchwork = Patchwork::new("one two three".to_string());
patchwork.patch(4..7, "zwei");
patchwork.patch(0..3, "один");
patchwork.patch(8..13, "3");
assert_eq(
str![[r#"
Patchwork {
text: "один zwei 3",
indels: [
(
0..3,
8,
),
(
4..7,
4,
),
(
8..13,
1,
),
],
}
"#]],
patchwork.to_debug(),
);
}
#[test]
fn test_locate() {
macro_rules! check_locate {
($( [[$s:literal]] ),* $(,)?) => {$({
let lit = stringify!($s);
let with_trailer = format!("{} \t]]\n", lit);
assert_eq!(locate_end(&with_trailer), Some(lit.len()));
})*};
}
// Check that we handle string literals containing "]]" correctly.
check_locate!(
[[r#"{ arr: [[1, 2], [3, 4]], other: "foo" } "#]],
[["]]"]],
[["\"]]"]],
[[r#""]]"#]],
);
// Check `str![[ ]]` as well.
assert_eq!(locate_end("]]"), Some(0));
}
#[test]
fn test_find_str_lit_len() {
macro_rules! check_str_lit_len {
($( $s:literal ),* $(,)?) => {$({
let lit = stringify!($s);
assert_eq!(find_str_lit_len(lit), Some(lit.len()));
})*}
}
check_str_lit_len![
r##"foa\""#"##,
r##"
asdf][]]""""#
"##,
"",
"\"",
"\"\"",
"#\"#\"#",
];
}
}