weavatrix_edit/application/
prepared.rs1use crate::error::{EditError, ErrorCode};
2use crate::model::Provenance;
3
4use core::ops::Range;
5
6use super::{
7 PreparedEdit, ProvenanceSet,
8 ranges::{prepared_output_size, sort_prepared, verify_ranges},
9 stream::EditChunks,
10 writer::{WriteSummary, write_prepared},
11};
12
13#[derive(Clone, Debug, Eq, PartialEq)]
15pub struct AppliedText {
16 pub text: String,
17 pub edits_applied: usize,
18 pub bytes_before: usize,
19 pub bytes_after: usize,
20}
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum OffsetBias {
25 Left,
26 Right,
27}
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct PreparedChange<'change> {
32 pub source_range: Range<usize>,
33 pub output_range: Range<usize>,
34 pub before: &'change str,
35 pub after: &'change str,
36 pub input_order: usize,
37 provenance: &'change ProvenanceSet,
38}
39
40impl PreparedChange<'_> {
41 #[must_use]
43 pub fn provenance(&self) -> &Provenance {
44 &self.provenance.primary
45 }
46
47 pub fn provenances(&self) -> impl Iterator<Item = &Provenance> {
49 core::iter::once(&self.provenance.primary).chain(self.provenance.additional.iter())
50 }
51
52 #[must_use]
53 pub fn provenance_count(&self) -> usize {
54 1 + self.provenance.additional.len()
55 }
56}
57
58#[derive(Clone, Debug)]
60pub struct PreparedChanges<'change> {
61 source: &'change str,
62 edits: core::slice::Iter<'change, PreparedEdit>,
63 source_cursor: usize,
64 output_cursor: usize,
65}
66
67impl<'change> Iterator for PreparedChanges<'change> {
68 type Item = PreparedChange<'change>;
69
70 fn next(&mut self) -> Option<Self::Item> {
71 let edit = self.edits.next()?;
72 let unchanged = edit.start - self.source_cursor;
73 let output_start = self.output_cursor + unchanged;
74 let output_end = output_start + edit.after.len();
75 self.source_cursor = self.source_cursor.max(edit.end);
76 self.output_cursor = output_end;
77 Some(PreparedChange {
78 source_range: edit.start..edit.end,
79 output_range: output_start..output_end,
80 before: &self.source[edit.start..edit.end],
81 after: &edit.after,
82 input_order: edit.order,
83 provenance: &edit.provenance,
84 })
85 }
86
87 fn size_hint(&self) -> (usize, Option<usize>) {
88 self.edits.size_hint()
89 }
90}
91
92impl ExactSizeIterator for PreparedChanges<'_> {}
93impl core::iter::FusedIterator for PreparedChanges<'_> {}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
97pub struct ChangeSummary {
98 pub edits: usize,
99 pub bytes_before: usize,
100 pub bytes_after: usize,
101 pub removed_bytes: usize,
102 pub inserted_bytes: usize,
103}
104
105#[derive(Clone, Debug)]
107pub struct PreparedEdits<'source> {
108 source: &'source str,
109 edits: Vec<PreparedEdit>,
110 output_size: usize,
111 max_edits: usize,
112 max_output_size: usize,
113}
114
115impl<'source> PreparedEdits<'source> {
116 pub(super) fn from_validated_parts(
117 source: &'source str,
118 edits: Vec<PreparedEdit>,
119 output_size: usize,
120 max_edits: usize,
121 max_output_size: usize,
122 ) -> Self {
123 Self {
124 source,
125 edits,
126 output_size,
127 max_edits,
128 max_output_size,
129 }
130 }
131
132 #[must_use]
134 pub fn apply(&self) -> AppliedText {
135 let mut text = String::with_capacity(self.output_size);
136 let mut cursor = 0_usize;
137 for edit in &self.edits {
138 if edit.start > cursor {
139 text.push_str(&self.source[cursor..edit.start]);
140 cursor = edit.start;
141 }
142 text.push_str(&edit.after);
143 if edit.end > edit.start {
144 cursor = edit.end;
145 }
146 }
147 text.push_str(&self.source[cursor..]);
148 AppliedText {
149 bytes_before: self.source.len(),
150 bytes_after: text.len(),
151 edits_applied: self.edits.len(),
152 text,
153 }
154 }
155
156 #[must_use]
162 pub fn chunks(&self) -> EditChunks<'_> {
163 EditChunks::new(self.source, &self.edits)
164 }
165
166 #[must_use]
173 pub fn changes(&self) -> PreparedChanges<'_> {
174 PreparedChanges {
175 source: self.source,
176 edits: self.edits.iter(),
177 source_cursor: 0,
178 output_cursor: 0,
179 }
180 }
181
182 #[must_use]
184 pub fn change_summary(&self) -> ChangeSummary {
185 let removed_bytes = self.edits.iter().map(|edit| edit.end - edit.start).sum();
186 let inserted_bytes = self.edits.iter().map(|edit| edit.after.len()).sum();
187 ChangeSummary {
188 edits: self.edits.len(),
189 bytes_before: self.source.len(),
190 bytes_after: self.output_size,
191 removed_bytes,
192 inserted_bytes,
193 }
194 }
195
196 pub fn write_to<W: std::io::Write + ?Sized>(
204 &self,
205 writer: &mut W,
206 ) -> std::io::Result<WriteSummary> {
207 write_prepared(
208 writer,
209 self.chunks(),
210 self.source.len(),
211 self.edits.len(),
212 self.output_size,
213 )
214 }
215
216 pub fn apply_with_validator(
218 &self,
219 validator: impl FnOnce(&str) -> bool,
220 ) -> Result<AppliedText, EditError> {
221 let applied = self.apply();
222 if validator(&applied.text) {
223 Ok(applied)
224 } else {
225 Err(EditError::new(
226 ErrorCode::ValidationRejected,
227 "the output validator rejected the complete edit result",
228 ))
229 }
230 }
231
232 pub fn union(mut self, mut other: Self) -> Result<Self, EditError> {
236 if self.source != other.source {
237 return Err(EditError::new(
238 ErrorCode::InvalidEdit,
239 "prepared edits can only be merged over identical source text",
240 ));
241 }
242 self.max_edits = self.max_edits.min(other.max_edits);
243 self.max_output_size = self.max_output_size.min(other.max_output_size);
244 let order_base = self
245 .edits
246 .iter()
247 .map(|edit| edit.order)
248 .max()
249 .map_or(Ok(0), |order| {
250 order.checked_add(1).ok_or_else(|| {
251 EditError::new(ErrorCode::PlanTooLarge, "merged edit order overflow")
252 })
253 })?;
254 for edit in &mut other.edits {
255 edit.order = order_base.checked_add(edit.order).ok_or_else(|| {
256 EditError::new(ErrorCode::PlanTooLarge, "merged edit order overflow")
257 })?;
258 }
259 self.edits.extend(other.edits);
260 sort_prepared(&mut self.edits);
261 self.edits.dedup_by(|right, left| {
262 let identical = left.start != left.end
263 && left.start == right.start
264 && left.end == right.end
265 && left.after == right.after;
266 if identical {
267 let placeholder = ProvenanceSet::new(right.provenance.primary.clone());
268 let other = core::mem::replace(&mut right.provenance, placeholder);
269 left.provenance.extend(other);
270 }
271 identical
272 });
273 if self.edits.len() > self.max_edits {
274 return Err(EditError::new(
275 ErrorCode::PlanTooLarge,
276 "merged edit count exceeds the application limit",
277 ));
278 }
279 verify_ranges(
280 self.edits
281 .iter()
282 .map(|edit| (edit.start, edit.end, edit.order)),
283 )?;
284 self.output_size =
285 prepared_output_size(self.source.len(), &self.edits, self.max_output_size)?;
286 Ok(self)
287 }
288
289 #[must_use]
291 pub fn invalidates_offset(&self, offset: usize) -> bool {
292 self.edits
293 .iter()
294 .any(|edit| edit.start < offset && offset < edit.end)
295 }
296
297 #[must_use]
299 pub fn map_offset_forward(&self, offset: usize, bias: OffsetBias) -> Option<usize> {
300 if offset > self.source.len() || !self.source.is_char_boundary(offset) {
301 return None;
302 }
303 let mut delta = 0_i128;
304 for edit in &self.edits {
305 if offset < edit.start {
306 break;
307 }
308 let mapped_start = shifted(edit.start, delta)?;
309 if edit.start < offset && offset < edit.end {
310 return None;
311 }
312 if edit.start == edit.end && offset == edit.start {
313 if bias == OffsetBias::Left {
314 return Some(mapped_start);
315 }
316 delta += i128::try_from(edit.after.len()).ok()?;
317 continue;
318 }
319 if offset == edit.start && edit.end > edit.start {
320 return Some(match bias {
321 OffsetBias::Left => mapped_start,
322 OffsetBias::Right => mapped_start.checked_add(edit.after.len())?,
323 });
324 }
325 if offset >= edit.end {
326 delta += i128::try_from(edit.after.len()).ok()?
327 - i128::try_from(edit.end - edit.start).ok()?;
328 }
329 }
330 shifted(offset, delta)
331 }
332
333 #[must_use]
334 pub fn len(&self) -> usize {
335 self.edits.len()
336 }
337
338 #[must_use]
340 pub fn bytes_before(&self) -> usize {
341 self.source.len()
342 }
343
344 #[must_use]
346 pub fn bytes_after(&self) -> usize {
347 self.output_size
348 }
349
350 #[must_use]
351 pub fn is_empty(&self) -> bool {
352 self.edits.is_empty()
353 }
354}
355
356fn shifted(offset: usize, delta: i128) -> Option<usize> {
357 usize::try_from(i128::try_from(offset).ok()?.checked_add(delta)?).ok()
358}