Skip to main content

substrate_data/forms/patch/
rewrite.rs

1//! [`patch`]: a document changed to hold a new value, the rest kept as it was.
2
3use super::format::{INDENT, format, quoted, same};
4use crate::model::value::{Map, Value};
5use crate::text::Spans;
6
7/// `text` changed to hold `next`, the rest as it was. Maps and lists are
8/// edited entry by entry (a kept entry keeps the comments before it; a
9/// removed one takes them along). Text that doesn't parse is written anew.
10pub 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
27/// The indent of the line `at` is on.
28fn 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
42/// Past whitespace and comments from `i`.
43fn 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
59/// A member as rewritten: its text, then what followed it (a comment) — the comma goes between.
60struct 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
77/// A container's members as text: each one's lead (the comments above it)
78/// and trail (after it, up to its comma); whether the last has a comma;
79/// what's left before the bracket.
80struct 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        // A comment on the member's own line stays with it; the rest goes to the next one.
95        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
123/// The new text of the value `old` (at `spans`) holding `next`.
124fn 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    // The old entry last written: a new key right after it, in the place of one that's gone, is that one renamed.
162    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        // Added or removed items: the others keep their text.
214        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}