1use std::collections::hash_map::DefaultHasher;
7use std::hash::{Hash, Hasher};
8
9pub fn anchor_of(line: &str) -> String {
14 let mut hasher = DefaultHasher::new();
15 line.trim().hash(&mut hasher);
16 format!("{:08x}", hasher.finish() as u32)
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct AnchoredLine {
22 pub anchor: String,
23 pub text: String,
24}
25
26pub fn anchored_lines(source: &str) -> Vec<AnchoredLine> {
28 source
29 .lines()
30 .map(|text| AnchoredLine {
31 anchor: anchor_of(text),
32 text: text.to_string(),
33 })
34 .collect()
35}
36
37pub fn render_anchored(source: &str) -> String {
40 anchored_lines(source)
41 .iter()
42 .map(|l| format!("{}\u{2502} {}", l.anchor, l.text))
43 .collect::<Vec<_>>()
44 .join("\n")
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum EditOp {
50 Replace(String),
52 InsertAfter(String),
54 InsertBefore(String),
56 Delete,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Edit {
63 pub anchor: String,
64 pub op: EditOp,
65}
66
67impl Edit {
68 pub fn replace(anchor: impl Into<String>, text: impl Into<String>) -> Self {
69 Edit {
70 anchor: anchor.into(),
71 op: EditOp::Replace(text.into()),
72 }
73 }
74 pub fn insert_after(anchor: impl Into<String>, text: impl Into<String>) -> Self {
75 Edit {
76 anchor: anchor.into(),
77 op: EditOp::InsertAfter(text.into()),
78 }
79 }
80 pub fn insert_before(anchor: impl Into<String>, text: impl Into<String>) -> Self {
81 Edit {
82 anchor: anchor.into(),
83 op: EditOp::InsertBefore(text.into()),
84 }
85 }
86 pub fn delete(anchor: impl Into<String>) -> Self {
87 Edit {
88 anchor: anchor.into(),
89 op: EditOp::Delete,
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum AnchorError {
97 NotFound(String),
99 Ambiguous { anchor: String, count: usize },
101}
102
103impl std::fmt::Display for AnchorError {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 AnchorError::NotFound(a) => write!(f, "no line matches anchor {a}"),
107 AnchorError::Ambiguous { anchor, count } => {
108 write!(f, "anchor {anchor} matches {count} lines (ambiguous)")
109 }
110 }
111 }
112}
113
114impl std::error::Error for AnchorError {}
115
116pub fn apply_edits(source: &str, edits: &[Edit]) -> Result<String, AnchorError> {
122 let lines: Vec<&str> = source.lines().collect();
123
124 let mut resolved: Vec<(usize, &EditOp)> = Vec::with_capacity(edits.len());
126 for edit in edits {
127 let matches: Vec<usize> = lines
128 .iter()
129 .enumerate()
130 .filter(|(_, l)| anchor_of(l) == edit.anchor)
131 .map(|(i, _)| i)
132 .collect();
133 match matches.as_slice() {
134 [] => return Err(AnchorError::NotFound(edit.anchor.clone())),
135 [i] => resolved.push((*i, &edit.op)),
136 many => {
137 return Err(AnchorError::Ambiguous {
138 anchor: edit.anchor.clone(),
139 count: many.len(),
140 })
141 }
142 }
143 }
144
145 let mut by_index: std::collections::HashMap<usize, Vec<&EditOp>> =
147 std::collections::HashMap::new();
148 for (i, op) in resolved {
149 by_index.entry(i).or_default().push(op);
150 }
151
152 let mut out: Vec<String> = Vec::with_capacity(lines.len());
153 for (i, line) in lines.iter().enumerate() {
154 let ops = by_index.get(&i);
155 if let Some(ops) = ops {
157 for op in ops {
158 if let EditOp::InsertBefore(text) = op {
159 out.extend(text.lines().map(String::from));
160 }
161 }
162 }
163 let mut emitted = false;
165 if let Some(ops) = ops {
166 for op in ops {
167 match op {
168 EditOp::Replace(text) => {
169 out.extend(text.lines().map(String::from));
170 emitted = true;
171 }
172 EditOp::Delete => emitted = true,
173 _ => {}
174 }
175 }
176 }
177 if !emitted {
178 out.push((*line).to_string());
179 }
180 if let Some(ops) = ops {
182 for op in ops {
183 if let EditOp::InsertAfter(text) = op {
184 out.extend(text.lines().map(String::from));
185 }
186 }
187 }
188 }
189
190 let mut result = out.join("\n");
191 if source.ends_with('\n') {
192 result.push('\n');
193 }
194 Ok(result)
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 const SRC: &str = "fn main() {\n let x = 1;\n println!(\"{x}\");\n}\n";
202
203 #[test]
204 fn anchor_is_indentation_insensitive_and_stable() {
205 assert_eq!(anchor_of(" let x = 1;"), anchor_of("let x = 1;"));
206 assert_eq!(anchor_of("let x = 1;").len(), 8);
207 }
208
209 #[test]
210 fn replace_targets_the_anchored_line() {
211 let anchor = anchor_of(" let x = 1;");
212 let out = apply_edits(SRC, &[Edit::replace(anchor, " let x = 42;")]).unwrap();
213 assert!(out.contains("let x = 42;"));
214 assert!(!out.contains("let x = 1;"));
215 assert!(out.ends_with('\n'));
216 }
217
218 #[test]
219 fn insert_after_and_before() {
220 let anchor = anchor_of(" let x = 1;");
221 let out = apply_edits(SRC, &[Edit::insert_after(&anchor, " let y = 2;")]).unwrap();
222 let lines: Vec<&str> = out.lines().collect();
223 let xi = lines.iter().position(|l| l.contains("let x = 1;")).unwrap();
224 assert!(lines[xi + 1].contains("let y = 2;"));
225 }
226
227 #[test]
228 fn delete_removes_the_line() {
229 let anchor = anchor_of(" println!(\"{x}\");");
230 let out = apply_edits(SRC, &[Edit::delete(anchor)]).unwrap();
231 assert!(!out.contains("println!"));
232 }
233
234 #[test]
235 fn unmatched_anchor_fails_the_batch() {
236 let err = apply_edits(SRC, &[Edit::replace("deadbeef", "x")]).unwrap_err();
237 assert!(matches!(err, AnchorError::NotFound(_)));
238 }
239
240 #[test]
241 fn ambiguous_anchor_is_rejected() {
242 let src = "dup\ndup\n";
243 let err = apply_edits(src, &[Edit::replace(anchor_of("dup"), "x")]).unwrap_err();
244 assert!(matches!(err, AnchorError::Ambiguous { count: 2, .. }));
245 }
246
247 #[test]
248 fn render_anchored_has_gutter() {
249 let rendered = render_anchored("hello");
250 assert!(rendered.starts_with(&anchor_of("hello")));
251 assert!(rendered.contains('\u{2502}'));
252 }
253}