substrate_data/forms/patch/
rewrite.rs1use super::format::{INDENT, format, quoted, same};
4use crate::model::value::{Map, Value};
5use crate::text::Spans;
6
7pub fn patch(text: &str, next: &Value) -> String {
11 let Ok(doc) = crate::text::parse(text) else {
12 return format!("{}\n", format(next, 0));
13 };
14 let (start, end) = range(&doc.spans);
15 format!(
16 "{}{}{}",
17 &text[..start],
18 rewrite(text, &doc.value, &doc.spans, next),
19 &text[end..]
20 )
21}
22
23fn range(s: &Spans) -> (usize, usize) {
24 (s.span.start as usize, s.span.end as usize)
25}
26
27fn line_indent(text: &str, at: usize) -> &str {
29 let start = text[..at].rfind('\n').map_or(0, |i| i + 1);
30 let line = &text[start..at];
31 &line[..line.len() - line.trim_start_matches([' ', '\t']).len()]
32}
33
34fn level_of(indent: &str) -> usize {
35 indent
36 .chars()
37 .map(|c| if c == '\t' { INDENT.len() } else { 1 })
38 .sum::<usize>()
39 .div_ceil(INDENT.len())
40}
41
42fn skip_blank(text: &str, mut i: usize) -> usize {
44 let b = text.as_bytes();
45 loop {
46 match (b.get(i), b.get(i + 1)) {
47 (Some(b' ' | b'\t' | b'\n' | b'\r'), _) => i += 1,
48 (Some(b'/'), Some(b'/')) => i = text[i..].find('\n').map_or(text.len(), |n| i + n),
49 (Some(b'/'), Some(b'*')) => {
50 i = text[i + 2..]
51 .find("*/")
52 .map_or(text.len(), |n| i + 2 + n + 2);
53 }
54 _ => return i,
55 }
56 }
57}
58
59struct Member {
61 body: String,
62 trail: String,
63}
64
65fn join(out: &[Member], trailing_comma: bool) -> String {
66 let mut s = String::new();
67 for (i, m) in out.iter().enumerate() {
68 s.push_str(&m.body);
69 if i + 1 < out.len() || trailing_comma {
70 s.push(',');
71 }
72 s.push_str(&m.trail);
73 }
74 s
75}
76
77struct Members {
81 segments: Vec<(String, String)>,
82 comma: bool,
83 tail: String,
84}
85
86fn members(text: &str, node: (usize, usize), spans: &[(usize, usize)]) -> Members {
87 let mut segments = Vec::with_capacity(spans.len());
88 let mut at = node.0 + 1;
89 let mut comma = false;
90 for &(start, end) in spans {
91 let lead = text[at..start].to_string();
92 let after = skip_blank(text, end);
93 comma = text.as_bytes().get(after) == Some(&b',');
94 let eol = text[end..].find('\n').map(|n| end + n);
96 let stop = if comma {
97 after
98 } else {
99 eol.map_or(after, |e| after.min(e))
100 };
101 let mut trail = text[end..stop].to_string();
102 at = if comma { after + 1 } else { stop };
103 if comma {
104 let line_end = text[at..].find('\n').map_or(text.len(), |n| at + n);
105 let rest = &text[at..line_end];
106 let t = rest.trim();
107 let comment = t.starts_with("//")
108 || (t.starts_with("/*") && t.ends_with("*/") && t.matches("*/").count() == 1);
109 if comment && line_end < node.1 {
110 trail.push_str(rest);
111 at = line_end;
112 }
113 }
114 segments.push((lead, trail));
115 }
116 Members {
117 segments,
118 comma,
119 tail: text[at..node.1 - 1].to_string(),
120 }
121}
122
123fn rewrite(text: &str, old: &Value, spans: &Spans, next: &Value) -> String {
125 let node = range(spans);
126 if same(old, next) {
127 return text[node.0..node.1].to_string();
128 }
129 let level = level_of(line_indent(text, node.0));
130 let fresh = || format(next, level);
131 if !text[node.0..node.1].contains('\n') {
132 return fresh();
133 }
134 match (old, next) {
135 (Value::Map(old), Value::Map(next))
136 if !old.is_empty() && spans.children.len() == old.len() =>
137 {
138 rewrite_map(text, node, old, spans, next)
139 }
140 (Value::List(old), Value::List(next))
141 if !old.is_empty() && spans.children.len() == old.len() =>
142 {
143 rewrite_list(text, node, old, spans, next)
144 }
145 _ => fresh(),
146 }
147}
148
149fn rewrite_map(text: &str, node: (usize, usize), old: &Map, spans: &Spans, next: &Map) -> String {
150 let keys: Vec<&str> = old.keys().collect();
151 let key_start = |c: &Spans| c.key.map_or(c.span.start, |k| k.start) as usize;
152 let ranges: Vec<(usize, usize)> = spans
153 .children
154 .iter()
155 .map(|c| (key_start(c), c.span.end as usize))
156 .collect();
157 let parts = members(text, node, &ranges);
158 let pad = line_indent(text, ranges[0].0);
159 let gone = |i: usize| i < keys.len() && next.get(keys[i]).is_none();
160 let mut out = Vec::with_capacity(next.len());
161 let mut last: Option<usize> = None;
163 for (k, v) in next.iter() {
164 let after = last.map_or(0, |l| l + 1);
165 let found = keys.iter().position(|o| *o == k);
166 let renamed = found.is_none() && gone(after);
167 let Some(i) = found.or(renamed.then_some(after)) else {
168 out.push(Member {
169 body: format!("\n{pad}{}: {}", quoted(k), format(v, level_of(pad))),
170 trail: String::new(),
171 });
172 continue;
173 };
174 last = Some(i);
175 let child = &spans.children[i];
176 let value_start = child.span.start as usize;
177 let key = if renamed {
178 let key_end = child.key.map_or(value_start, |s| s.end as usize);
179 format!("{}{}", quoted(k), &text[key_end..value_start])
180 } else {
181 text[ranges[i].0..value_start].to_string()
182 };
183 let old_value = old.value_at(i).unwrap_or(&Value::Null);
184 let (lead, trail) = &parts.segments[i];
185 out.push(Member {
186 body: format!("{lead}{key}{}", rewrite(text, old_value, child, v)),
187 trail: trail.clone(),
188 });
189 }
190 format!("{{{}{}}}", join(&out, parts.comma), parts.tail)
191}
192
193fn rewrite_list(
194 text: &str,
195 node: (usize, usize),
196 old: &[Value],
197 spans: &Spans,
198 next: &[Value],
199) -> String {
200 let ranges: Vec<(usize, usize)> = spans.children.iter().map(range).collect();
201 let parts = members(text, node, &ranges);
202 let pad = line_indent(text, ranges[0].0);
203 let item = |i: usize, v: &Value| {
204 let (lead, trail) = &parts.segments[i];
205 Member {
206 body: format!("{lead}{}", rewrite(text, &old[i], &spans.children[i], v)),
207 trail: trail.clone(),
208 }
209 };
210 let out: Vec<Member> = if next.len() == old.len() {
211 next.iter().enumerate().map(|(i, v)| item(i, v)).collect()
212 } else {
213 let mut from = 0;
215 next.iter()
216 .map(|v| match (from..old.len()).find(|&k| same(&old[k], v)) {
217 Some(i) => {
218 from = i + 1;
219 item(i, v)
220 }
221 None => Member {
222 body: format!("\n{pad}{}", format(v, level_of(pad))),
223 trail: String::new(),
224 },
225 })
226 .collect()
227 };
228 format!("[{}{}]", join(&out, parts.comma), parts.tail)
229}