1use std::path::Path;
10
11use serde::{Deserialize, Serialize};
12use similar::TextDiff;
13
14use crate::{AgdError, ByteSpan, ErrorCode};
15
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct Patch {
18 pub span: ByteSpan,
19 pub replacement: String,
20}
21
22#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
27pub struct StoredPatch {
28 pub start: usize,
29 pub end: usize,
30 pub before: String,
31 pub after: String,
32 #[serde(default)]
33 pub context_before: Option<String>,
34 #[serde(default)]
35 pub context_after: Option<String>,
36}
37
38pub fn apply_patches(
39 source: &str,
40 mut patches: Vec<Patch>,
41) -> Result<(String, Vec<StoredPatch>), AgdError> {
42 patches.sort_by_key(|patch| (patch.span.start, patch.span.end));
43 let mut cursor = 0;
44 let mut post_cursor = 0;
45 let mut output = String::with_capacity(source.len());
46 let mut stored = Vec::with_capacity(patches.len());
47 for patch in patches {
48 if patch.span.start < cursor
49 || patch.span.end < patch.span.start
50 || patch.span.end > source.len()
51 {
52 return Err(AgdError::new(
53 ErrorCode::InvalidPatch,
54 format!(
55 "patch {}..{} overlaps or exceeds the source",
56 patch.span.start, patch.span.end
57 ),
58 ));
59 }
60 if !source.is_char_boundary(patch.span.start) || !source.is_char_boundary(patch.span.end) {
61 return Err(AgdError::new(
62 ErrorCode::InvalidPatch,
63 "patch is not on UTF-8 boundaries",
64 ));
65 }
66 output.push_str(&source[cursor..patch.span.start]);
67 post_cursor += patch.span.start - cursor;
68 let post_start = post_cursor;
69 let before = source[patch.span.start..patch.span.end].to_string();
70 output.push_str(&patch.replacement);
71 post_cursor += patch.replacement.len();
72 stored.push(StoredPatch {
73 start: post_start,
74 end: post_cursor,
75 before,
76 after: patch.replacement,
77 context_before: None,
78 context_after: None,
79 });
80 cursor = patch.span.end;
81 }
82 output.push_str(&source[cursor..]);
83 for patch in &mut stored {
84 let context_start = line_start(&output, line_start(&output, patch.start).saturating_sub(1));
85 let context_end = line_end(&output, line_end(&output, patch.end) + 1);
86 patch.context_before = Some(output[context_start..patch.start].to_string());
87 patch.context_after = Some(output[patch.end..context_end].to_string());
88 }
89 Ok((output, stored))
90}
91
92fn line_start(text: &str, offset: usize) -> usize {
93 text[..offset.min(text.len())]
94 .rfind('\n')
95 .map_or(0, |index| index + 1)
96}
97
98fn line_end(text: &str, offset: usize) -> usize {
100 if offset >= text.len() {
101 return text.len();
102 }
103 text[offset..]
104 .find('\n')
105 .map_or(text.len(), |index| offset + index + 1)
106}
107
108pub(super) fn changed_lines_patch(before: &str, after: &str) -> Patch {
111 let mut prefix = before
112 .bytes()
113 .zip(after.bytes())
114 .take_while(|(left, right)| left == right)
115 .count();
116 prefix = line_start(before, prefix);
117 let mut suffix = before
118 .bytes()
119 .rev()
120 .zip(after.bytes().rev())
121 .take_while(|(left, right)| left == right)
122 .count()
123 .min(before.len() - prefix)
124 .min(after.len() - prefix);
125 let tail = line_end_at_or_after(before, before.len() - suffix);
126 suffix = before.len() - tail;
127 while !before.is_char_boundary(before.len() - suffix)
128 || !after.is_char_boundary(after.len() - suffix)
129 {
130 suffix -= 1;
131 }
132 Patch {
133 span: ByteSpan {
134 start: prefix,
135 end: before.len() - suffix,
136 },
137 replacement: after[prefix..after.len() - suffix].to_string(),
138 }
139}
140
141fn line_end_at_or_after(text: &str, offset: usize) -> usize {
143 if offset == 0 || offset >= text.len() || text.as_bytes()[offset - 1] == b'\n' {
144 return offset;
145 }
146 line_end(text, offset)
147}
148
149pub(crate) fn line_ending(source: &str) -> &'static str {
151 if source.contains("\r\n") {
152 "\r\n"
153 } else {
154 "\n"
155 }
156}
157
158pub(super) fn unified_diff(path: &Path, before: &str, after: &str) -> String {
159 TextDiff::from_lines(before, after)
160 .unified_diff()
161 .header(&path.display().to_string(), &path.display().to_string())
162 .to_string()
163}