1#[derive(Clone, Debug, PartialEq, Eq)]
4pub struct SubstituteCmd {
5 pub pattern: String,
6 pub replacement: String,
7 pub global_file: bool,
9 pub global_line: bool,
11 pub confirm: bool,
12}
13
14pub fn parse_substitute(input: &str) -> Option<SubstituteCmd> {
16 let input = input.trim();
17 let (global_file, rest) = if let Some(r) = input.strip_prefix("%s") {
18 (true, r.trim_start())
19 } else if let Some(r) = input.strip_prefix('s') {
20 if r.starts_with(|c: char| c.is_alphanumeric()) {
22 return None;
23 }
24 (false, r.trim_start())
25 } else {
26 return None;
27 };
28
29 if rest.is_empty() {
30 return None;
31 }
32 let delim = rest.chars().next()?;
33 if delim.is_whitespace() {
34 return None;
35 }
36 let rest = &rest[delim.len_utf8()..];
37
38 let mut parts: Vec<String> = Vec::new();
39 let mut cur = String::new();
40 let mut escaped = false;
41 for ch in rest.chars() {
42 if escaped {
43 cur.push(ch);
44 escaped = false;
45 continue;
46 }
47 if ch == '\\' {
48 escaped = true;
49 continue;
50 }
51 if ch == delim {
52 parts.push(std::mem::take(&mut cur));
53 } else {
54 cur.push(ch);
55 }
56 }
57
58 let flags = if parts.len() >= 2 {
61 if parts.len() >= 3 {
62 let mut f = parts[2].clone();
64 f.push_str(&cur);
65 f
66 } else {
67 cur
68 }
69 } else {
70 if !cur.is_empty() {
72 parts.push(cur);
73 }
74 String::new()
75 };
76
77 let pattern = parts.first()?.clone();
78 if pattern.is_empty() {
79 return None;
80 }
81 let replacement = parts.get(1).cloned().unwrap_or_default();
82 let global_line = flags.contains('g');
83 let confirm = flags.contains('c');
84
85 Some(SubstituteCmd {
86 pattern,
87 replacement,
88 global_file,
89 global_line,
90 confirm,
91 })
92}
93
94pub fn apply_substitute(
96 lines: &[String],
97 cmd: &SubstituteCmd,
98 cursor_row: usize,
99) -> (Vec<String>, usize) {
100 let mut count = 0usize;
101 let mut out = lines.to_vec();
102 if out.is_empty() {
103 return (out, 0);
104 }
105
106 let rows: Vec<usize> = if cmd.global_file {
107 (0..out.len()).collect()
108 } else {
109 vec![cursor_row.min(out.len() - 1)]
110 };
111
112 for row in rows {
113 let (new_line, n) =
114 replace_on_line(&out[row], &cmd.pattern, &cmd.replacement, cmd.global_line);
115 if n > 0 {
116 out[row] = new_line;
117 count += n;
118 }
119 }
120 (out, count)
121}
122
123fn replace_on_line(line: &str, pat: &str, repl: &str, all: bool) -> (String, usize) {
124 if pat.is_empty() {
125 return (line.to_string(), 0);
126 }
127 if all {
128 let n = line.matches(pat).count();
129 (line.replace(pat, repl), n)
130 } else if let Some(idx) = line.find(pat) {
131 let mut s = String::with_capacity(line.len() + repl.len());
132 s.push_str(&line[..idx]);
133 s.push_str(repl);
134 s.push_str(&line[idx + pat.len()..]);
135 (s, 1)
136 } else {
137 (line.to_string(), 0)
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn parse_basic() {
147 let c = parse_substitute("s/foo/bar/").unwrap();
148 assert_eq!(c.pattern, "foo");
149 assert_eq!(c.replacement, "bar");
150 assert!(!c.global_file);
151 assert!(!c.global_line);
152 }
153
154 #[test]
155 fn parse_global() {
156 let c = parse_substitute("%s/a/b/g").unwrap();
157 assert!(c.global_file);
158 assert!(c.global_line);
159 assert_eq!(c.pattern, "a");
160 assert_eq!(c.replacement, "b");
161 }
162
163 #[test]
164 fn apply_line() {
165 let lines = vec!["foo bar foo".into()];
166 let cmd = parse_substitute("s/foo/X/").unwrap();
167 let (out, n) = apply_substitute(&lines, &cmd, 0);
168 assert_eq!(n, 1);
169 assert_eq!(out[0], "X bar foo");
170 }
171
172 #[test]
173 fn apply_global() {
174 let lines = vec!["a a".into(), "a".into()];
175 let cmd = parse_substitute("%s/a/b/g").unwrap();
176 let (out, n) = apply_substitute(&lines, &cmd, 0);
177 assert_eq!(n, 3);
178 assert_eq!(out[0], "b b");
179 assert_eq!(out[1], "b");
180 }
181}