1use kernel::{AxCode, AxError, B3Hash};
18use serde::{Deserialize, Serialize};
19
20use crate::encoding::{Encoding, Reading};
21use crate::span::{Span, offset};
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27pub struct TextEdit {
28 pub span: Span,
29 pub text: String,
30}
31
32pub fn save(source: &[u8], baseline: B3Hash, edits: &[TextEdit]) -> Result<Applied, AxError> {
42 if B3Hash::digest(source) != baseline {
43 return Err(moved(baseline));
44 }
45 saved_on(source, baseline, edits)
46}
47
48pub(crate) fn saved_on(
51 source: &[u8],
52 baseline: B3Hash,
53 edits: &[TextEdit],
54) -> Result<Applied, AxError> {
55 let Reading::Text(encoding) = Reading::of(source) else {
56 return Err(not_text(
57 "the version is not text in any encoding this city reads",
58 ));
59 };
60 let bytes = edits
61 .iter()
62 .map(|edit| {
63 on_characters(encoding, source, edit.span).map(|()| Edit {
64 span: edit.span,
65 bytes: encoding.encode(&edit.text),
66 })
67 })
68 .collect::<Result<Vec<Edit>, AxError>>()?;
69 let applied = Transaction::new(baseline, bytes)?.splice(source)?;
70 if Reading::of(&applied.bytes) != Reading::Text(encoding) {
71 return Err(not_text(
72 "the saved bytes would not read as text in the version's own encoding",
73 ));
74 }
75 Ok(applied)
76}
77
78fn on_characters(encoding: Encoding, source: &[u8], span: Span) -> Result<(), AxError> {
80 let starts = |at: u64| {
81 usize::try_from(at)
82 .is_ok_and(|index| index == source.len() || encoding.begins_at(source, index, 0))
83 };
84 if starts(span.start()) && starts(span.end()) {
85 Ok(())
86 } else {
87 Err(not_text(&format!(
88 "the edit at {}..{} starts or ends inside a character",
89 span.start(),
90 span.end()
91 )))
92 }
93}
94
95fn not_text(why: &str) -> AxError {
96 AxError::failure(AxCode::InvalidArgs, "save a document", why.to_owned())
97 .with_recovery("read the document again: its answer says whether and how it reads as text")
98}
99
100fn moved(baseline: B3Hash) -> AxError {
101 AxError::failure(
102 AxCode::VersionConflict,
103 "save a document",
104 format!("the document is no longer version {baseline}"),
105 )
106 .with_recovery("read the document again and make the change on what it says now")
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
111pub struct Edit {
112 pub span: Span,
113 pub bytes: Vec<u8>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Transaction {
122 baseline: B3Hash,
123 edits: Vec<Edit>,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct Applied {
130 baseline: B3Hash,
131 bytes: Vec<u8>,
132 version: B3Hash,
133 undo: Transaction,
134}
135
136impl Transaction {
137 pub fn new(baseline: B3Hash, edits: Vec<Edit>) -> Result<Transaction, AxError> {
146 let tangled = edits
147 .iter()
148 .zip(edits.iter().skip(1))
149 .find_map(|(earlier, later)| {
150 (later.span.start() < earlier.span.end()).then_some(later.span)
151 });
152 if let Some(span) = tangled {
153 return Err(AxError::failure(
154 AxCode::InvalidArgs,
155 "save a document",
156 format!(
157 "the edit at {}..{} starts before the edit ahead of it ends",
158 span.start(),
159 span.end()
160 ),
161 )
162 .with_recovery("send the edits in document order, each apart from the next"));
163 }
164 Ok(Transaction { baseline, edits })
165 }
166
167 pub fn baseline(&self) -> B3Hash {
169 self.baseline
170 }
171
172 pub fn edits(&self) -> &[Edit] {
173 &self.edits
174 }
175
176 pub fn apply(&self, source: &[u8]) -> Result<Applied, AxError> {
184 if B3Hash::digest(source) != self.baseline {
185 return Err(moved(self.baseline));
186 }
187 self.splice(source)
188 }
189
190 fn splice(&self, source: &[u8]) -> Result<Applied, AxError> {
192 let mut bytes = Vec::with_capacity(source.len());
193 let mut undo = Vec::with_capacity(self.edits.len());
194 let mut cursor = 0_usize;
195 for edit in &self.edits {
196 let outside = || past_the_end(edit.span, source.len());
197 let (start, end) = edit.span.within(source.len()).ok_or_else(outside)?;
198 bytes.extend_from_slice(source.get(cursor..start).ok_or_else(outside)?);
199 let replaced = source.get(start..end).ok_or_else(outside)?;
200 let from = offset(bytes.len());
201 bytes.extend_from_slice(&edit.bytes);
202 undo.push(Edit {
203 span: Span::ordered(from, offset(bytes.len())),
204 bytes: replaced.to_vec(),
205 });
206 cursor = end;
207 }
208 bytes.extend_from_slice(
209 source
210 .get(cursor..)
211 .ok_or_else(|| past_the_end(Span::at(offset(cursor)), source.len()))?,
212 );
213 let version = B3Hash::digest(&bytes);
214 Ok(Applied {
215 baseline: self.baseline,
216 bytes,
217 version,
218 undo: Transaction {
219 baseline: version,
220 edits: undo,
221 },
222 })
223 }
224}
225
226impl Applied {
227 pub fn baseline(&self) -> B3Hash {
229 self.baseline
230 }
231
232 pub fn bytes(&self) -> &[u8] {
233 &self.bytes
234 }
235
236 pub fn version(&self) -> B3Hash {
238 self.version
239 }
240
241 pub fn undo(&self) -> &Transaction {
244 &self.undo
245 }
246}
247
248fn past_the_end(span: Span, len: usize) -> AxError {
249 AxError::failure(
250 AxCode::InvalidArgs,
251 "save a document",
252 format!(
253 "the edit at {}..{} reaches past the end of {len} bytes",
254 span.start(),
255 span.end()
256 ),
257 )
258 .with_recovery("read the document again: the edit was made on a longer version")
259}
260
261#[cfg(test)]
262#[allow(
263 clippy::unwrap_used,
264 clippy::expect_used,
265 clippy::panic,
266 clippy::indexing_slicing,
267 reason = "test code"
268)]
269mod tests;