1pub fn create_patched_content(
2 original_content: &str,
3 search_block: &str,
4 replace_block: &str,
5) -> Option<String> {
6 if let Some(patched) = try_exact_string_patch(original_content, search_block, replace_block) {
8 return Some(patched);
9 }
10
11 try_whitespace_flexible_patch(original_content, search_block, replace_block)
13}
14
15fn try_exact_string_patch(original: &str, search: &str, replace: &str) -> Option<String> {
16 if search.is_empty() && original.is_empty() {
18 return Some(replace.to_string());
19 }
20
21 if replace.is_empty() && search == original {
23 return Some(String::new());
24 }
25
26 if search.trim().is_empty() {
27 return None;
28 }
29
30 original
31 .find(search)
32 .map(|_| original.replacen(search, replace, 1))
33}
34
35fn try_whitespace_flexible_patch(original: &str, search: &str, replace: &str) -> Option<String> {
36 let original_lines: Vec<&str> = original.split_inclusive('\n').collect();
39 let search_lines: Vec<&str> = search.split_inclusive('\n').collect();
40 let replace_lines: Vec<&str> = replace.split_inclusive('\n').collect();
41
42 if search_lines.is_empty() || search_lines.len() > original_lines.len() {
43 return None;
44 }
45
46 let stripped_search: Vec<&str> = search_lines
47 .iter()
48 .map(|s| s.trim_end_matches(['\n', '\r']).trim())
49 .collect();
50 if stripped_search.iter().all(|s| s.is_empty()) {
51 return None;
52 }
53
54 let match_start_index = original_lines
56 .windows(search_lines.len())
57 .position(|window| {
58 window
59 .iter()
60 .map(|s| s.trim_end_matches(['\n', '\r']).trim())
62 .eq(stripped_search.iter().cloned())
63 });
64
65 let start_idx = match_start_index?;
66
67 let matched_chunk = &original_lines[start_idx..start_idx + search_lines.len()];
69 let original_indent = get_consistent_indentation(matched_chunk);
70 let replace_indent = get_consistent_indentation(&replace_lines);
71
72 let mut new_lines: Vec<String> = Vec::new();
73
74 new_lines.extend(original_lines[..start_idx].iter().map(|s| s.to_string()));
76
77 for line in replace_lines {
79 if line.trim().is_empty() {
82 new_lines.push(line.to_string());
83 continue;
84 }
85
86 let relative = if !replace_indent.is_empty() && line.starts_with(&replace_indent) {
87 &line[replace_indent.len()..]
88 } else {
89 line
90 };
91 new_lines.push(format!("{}{}", original_indent, relative));
92 }
93
94 new_lines.extend(
96 original_lines[start_idx + search_lines.len()..]
97 .iter()
98 .map(|s| s.to_string()),
99 );
100
101 Some(new_lines.concat())
102}
103
104fn get_consistent_indentation(lines: &[&str]) -> String {
105 let mut iter = lines.iter().filter(|l| !l.trim().is_empty()).cloned();
106
107 let first = match iter.next() {
108 Some(f) => f,
109 None => return String::new(),
110 };
111
112 let indent_len = first.len() - first.trim_start().len();
114 let mut common_indent = &first[..indent_len];
115
116 for line in iter {
117 let shared_len = common_indent
118 .char_indices()
119 .zip(line.chars())
120 .take_while(|((_, c1), c2)| c1 == c2)
121 .map(|((i, c), _)| i + c.len_utf8())
122 .last()
123 .unwrap_or(0);
124
125 common_indent = &common_indent[..shared_len];
126
127 if common_indent.is_empty() {
128 break;
129 }
130 }
131
132 common_indent.to_string()
133}