Skip to main content

sim_value/
edit.rs

1//! Side-effect-free text edit primitives.
2
3use sim_kernel::{Error, Result};
4
5/// Replaces `old` with `new` in `text`.
6///
7/// The pattern must be present. When `replace_all` is false, the pattern must
8/// occur exactly once so a caller cannot patch an unintended occurrence.
9pub fn edit(text: &str, old: &str, new: &str, replace_all: bool) -> Result<String> {
10    if old.is_empty() {
11        return Err(Error::Eval("edit: old pattern is empty".to_owned()));
12    }
13    let matches = text.matches(old).count();
14    match matches {
15        0 => Err(Error::Eval(format!("edit: pattern not found: {old:?}"))),
16        n if n > 1 && !replace_all => Err(Error::Eval(format!(
17            "edit: pattern is not unique ({n} matches); pass replace_all"
18        ))),
19        _ if replace_all => Ok(text.replace(old, new)),
20        _ => Ok(text.replacen(old, new, 1)),
21    }
22}
23
24/// Replaces a 1-based inclusive line range with `new`.
25///
26/// `new` is inserted exactly as provided. Callers that want the replacement to
27/// end in a newline include it in `new`.
28pub fn edit_lines(text: &str, start: usize, end: usize, new: &str) -> Result<String> {
29    if start == 0 {
30        return Err(Error::Eval(
31            "edit-lines: start must be at least 1".to_owned(),
32        ));
33    }
34    if end < start {
35        return Err(Error::Eval(
36            "edit-lines: end must be greater than or equal to start".to_owned(),
37        ));
38    }
39
40    let lines = text.split_inclusive('\n').collect::<Vec<_>>();
41    if end > lines.len() {
42        return Err(Error::Eval(format!(
43            "edit-lines: range {start}..{end} exceeds {} line(s)",
44            lines.len()
45        )));
46    }
47
48    let mut edited = String::new();
49    for line in &lines[..start - 1] {
50        edited.push_str(line);
51    }
52    edited.push_str(new);
53    for line in &lines[end..] {
54        edited.push_str(line);
55    }
56    Ok(edited)
57}