1use regex::{NoExpand, Regex};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct Site {
13 pub path: String,
14 pub line: usize,
15 pub before: String,
16 pub after: String,
17}
18
19pub fn edit_content(
43 path: &str,
44 content: &str,
45 re: &Regex,
46 replacement: &str,
47 literal: bool,
48) -> (String, usize, Vec<Site>) {
49 let mut out = String::with_capacity(content.len());
50 let mut count = 0usize;
51 let mut sites = Vec::new();
52
53 for (idx, segment) in content.split_inclusive('\n').enumerate() {
54 let (body, nl) = match segment.strip_suffix('\n') {
55 Some(b) => (b, "\n"),
56 None => (segment, ""),
57 };
58 let hits = re.find_iter(body).count();
59 if hits == 0 {
60 out.push_str(segment);
61 continue;
62 }
63 count += hits;
64 let new_body = if literal {
65 re.replace_all(body, NoExpand(replacement))
66 } else {
67 re.replace_all(body, replacement)
68 };
69 if new_body.as_ref() != body {
70 sites.push(Site {
71 path: path.to_string(),
72 line: idx + 1,
73 before: body.to_string(),
74 after: new_body.to_string(),
75 });
76 }
77 out.push_str(&new_body);
78 out.push_str(nl);
79 }
80
81 (out, count, sites)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::pattern::{self, PatternKind};
88
89 fn re(p: &str) -> (Regex, bool) {
90 (
91 pattern::compile(p).unwrap(),
92 !matches!(pattern::classify(p), PatternKind::Regex),
93 )
94 }
95
96 #[test]
97 fn preserves_untouched_lines_and_terminators() {
98 let (r, lit) = re("foo");
99 let (out, n, sites) = edit_content("f", "a\nfoo bar\n foo\n", &r, "X", lit);
100 assert_eq!(n, 2);
101 assert_eq!(out, "a\nX bar\n X\n");
102 assert_eq!(sites.len(), 2);
103 assert_eq!(sites[1].after, " X");
104 }
105
106 #[test]
107 fn missing_final_newline_is_preserved() {
108 let (r, lit) = re("a");
109 let (out, n, _) = edit_content("f", "a", &r, "b", lit);
110 assert_eq!((out.as_str(), n), ("b", 1));
111 }
112
113 #[test]
114 fn literal_find_does_not_expand_dollar_in_replacement() {
115 let (r, lit) = re("KEY");
116 let (out, _, _) = edit_content("f", "KEY\n", &r, "$1 cost", lit);
117 assert_eq!(out, "$1 cost\n");
118 }
119
120 #[test]
121 fn regex_find_expands_captures() {
122 let (r, lit) = re(r"v(\d+)");
123 let (out, n, _) = edit_content("f", "v12\n", &r, "ver${1}", lit);
124 assert_eq!((out.as_str(), n), ("ver12\n", 1));
125 }
126}