1use super::{Range, Result};
8use crate::core::errors::EditorError;
9use ass_core::parser::{script::ScriptDeltaOwned, Script};
10
11#[cfg(feature = "std")]
12use std::borrow::Cow;
13
14#[cfg(not(feature = "std"))]
15use alloc::{borrow::Cow, format, string::String, string::ToString};
16
17#[cfg(not(feature = "std"))]
18use alloc::vec::Vec;
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct DocumentChange<'a> {
23 pub range: Range,
25
26 pub new_text: Cow<'a, str>,
28
29 pub old_text: Cow<'a, str>,
31
32 #[cfg(feature = "std")]
34 pub timestamp: std::time::Instant,
35
36 pub change_id: u64,
38}
39
40#[derive(Debug)]
42pub struct IncrementalParser {
43 cached_script: Option<String>,
45
46 pending_changes: Vec<DocumentChange<'static>>,
48
49 next_change_id: u64,
51
52 reparse_threshold: usize,
54
55 bytes_changed: usize,
57}
58
59impl Default for IncrementalParser {
60 fn default() -> Self {
61 Self::new()
62 }
63}
64
65impl IncrementalParser {
66 pub fn new() -> Self {
68 Self {
69 cached_script: None,
70 pending_changes: Vec::new(),
71 next_change_id: 1,
72 reparse_threshold: 10_000, bytes_changed: 0,
74 }
75 }
76
77 pub fn set_reparse_threshold(&mut self, threshold: usize) {
79 self.reparse_threshold = threshold;
80 }
81
82 pub fn initialize_cache(&mut self, content: &str) {
85 self.cached_script = Some(content.to_string());
86 self.pending_changes.clear();
87 self.bytes_changed = 0;
88 }
89
90 pub fn has_cached_script(&self) -> bool {
92 self.cached_script.is_some()
93 }
94
95 pub fn with_cached_script<F, R>(&self, f: F) -> Result<R>
97 where
98 F: FnOnce(&Script) -> Result<R>,
99 {
100 let cached = self
101 .cached_script
102 .as_ref()
103 .ok_or_else(|| EditorError::command_failed("No cached script available"))?;
104 let script = Script::parse(cached).map_err(EditorError::from)?;
105 f(&script)
106 }
107
108 pub fn apply_change(
110 &mut self,
111 document_text: &str,
112 range: Range,
113 new_text: &str,
114 ) -> Result<ScriptDeltaOwned> {
115 if self.cached_script.is_none() || self.bytes_changed >= self.reparse_threshold {
117 return self.full_reparse(document_text);
118 }
119
120 if range.end.offset > document_text.len() || range.start.offset > range.end.offset {
122 return Err(EditorError::InvalidRange {
123 start: range.start.offset,
124 end: range.end.offset,
125 length: document_text.len(),
126 });
127 }
128
129 let start_is_valid = range.start.offset == 0
131 || range.start.offset == document_text.len()
132 || document_text.is_char_boundary(range.start.offset);
133 let end_is_valid = range.end.offset == 0
134 || range.end.offset == document_text.len()
135 || document_text.is_char_boundary(range.end.offset);
136
137 if !start_is_valid || !end_is_valid {
138 return Err(EditorError::command_failed(
141 "Edit range is not on valid UTF-8 character boundaries",
142 ));
143 }
144
145 let old_text = &document_text[range.start.offset..range.end.offset];
147 let (start_byte, end_byte) = (range.start.offset, range.end.offset);
148
149 let change = DocumentChange {
151 range,
152 new_text: Cow::Owned(new_text.to_string()),
153 old_text: Cow::Owned(old_text.to_string()),
154 #[cfg(feature = "std")]
155 timestamp: std::time::Instant::now(),
156 change_id: self.next_change_id,
157 };
158 self.next_change_id += 1;
159
160 let change_size = new_text.len().abs_diff(old_text.len());
162 self.bytes_changed += change_size;
163
164 self.pending_changes.push(change);
166
167 let byte_range = start_byte..end_byte;
170
171 let cached = self.cached_script.as_ref().ok_or_else(|| {
173 EditorError::command_failed("Cached script unavailable for incremental parsing")
174 })?;
175 let script = Script::parse(cached).map_err(EditorError::from)?;
176
177 match script.parse_partial(byte_range, new_text) {
179 Ok(delta) => {
180 self.update_cached_script(range, new_text)?;
182 Ok(delta)
183 }
184 Err(_e) => {
185 self.pending_changes.pop(); self.bytes_changed -= change_size;
188
189 #[cfg(feature = "std")]
191 eprintln!("Incremental parse failed, falling back to full parse: {_e}");
192
193 self.full_reparse(document_text)
194 }
195 }
196 }
197
198 pub fn full_reparse(&mut self, content: &str) -> Result<ScriptDeltaOwned> {
200 let new_script = Script::parse(content).map_err(EditorError::from)?;
202
203 let delta = if let Some(cached_content) = &self.cached_script {
205 let old_script = Script::parse(cached_content).map_err(EditorError::from)?;
206
207 let delta = ass_core::parser::calculate_delta(&old_script, &new_script);
209
210 let mut owned_delta = ScriptDeltaOwned {
212 added: Vec::new(),
213 modified: Vec::new(),
214 removed: Vec::new(),
215 new_issues: new_script.issues().to_vec(),
216 };
217
218 for section in delta.added {
220 owned_delta.added.push(format!("{section:?}"));
221 }
222
223 for (idx, section) in delta.modified {
225 owned_delta.modified.push((idx, format!("{section:?}")));
226 }
227
228 owned_delta.removed = delta.removed;
230
231 owned_delta
232 } else {
233 ScriptDeltaOwned {
235 added: new_script
236 .sections()
237 .iter()
238 .map(|s| format!("{s:?}"))
239 .collect(),
240 modified: Vec::new(),
241 removed: Vec::new(),
242 new_issues: new_script.issues().to_vec(),
243 }
244 };
245
246 self.cached_script = Some(content.to_string());
248 self.pending_changes.clear();
249 self.bytes_changed = 0;
250
251 Ok(delta)
252 }
253
254 pub fn clear_cache(&mut self) {
256 self.cached_script = None;
257 self.pending_changes.clear();
258 self.bytes_changed = 0;
259 self.next_change_id = 1;
260 }
261
262 pub fn pending_changes(&self) -> &[DocumentChange<'static>] {
264 &self.pending_changes
265 }
266
267 pub fn should_reparse(&self) -> bool {
269 self.bytes_changed >= self.reparse_threshold || self.pending_changes.len() > 50
270 }
271
272 fn update_cached_script(&mut self, range: Range, new_text: &str) -> Result<()> {
274 if let Some(cached) = &mut self.cached_script {
275 if range.start.offset > cached.len() || range.end.offset > cached.len() {
277 return Err(EditorError::InvalidRange {
278 start: range.start.offset,
279 end: range.end.offset,
280 length: cached.len(),
281 });
282 }
283
284 if !cached.is_char_boundary(range.start.offset)
286 || !cached.is_char_boundary(range.end.offset)
287 {
288 return Err(EditorError::command_failed(
289 "Cache update range is not on valid UTF-8 character boundaries",
290 ));
291 }
292
293 let mut result = String::with_capacity(
295 cached.len() - (range.end.offset - range.start.offset) + new_text.len(),
296 );
297
298 result.push_str(&cached[..range.start.offset]);
300
301 result.push_str(new_text);
303
304 if range.end.offset < cached.len() {
306 result.push_str(&cached[range.end.offset..]);
307 }
308
309 *cached = result;
310 }
311
312 Ok(())
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use crate::core::Position;
320 #[cfg(not(feature = "std"))]
321 use alloc::string::ToString;
322 #[cfg(not(feature = "std"))]
323 #[test]
324 fn test_incremental_parser_creation() {
325 let parser = IncrementalParser::new();
326 assert!(parser.cached_script.is_none());
327 assert!(parser.pending_changes.is_empty());
328 assert_eq!(parser.bytes_changed, 0);
329 }
330
331 #[test]
332 fn test_document_change_tracking() {
333 let change = DocumentChange {
334 range: Range::new(Position::new(0), Position::new(5)),
335 new_text: Cow::Borrowed("Hello"),
336 old_text: Cow::Borrowed("World"),
337 #[cfg(feature = "std")]
338 timestamp: std::time::Instant::now(),
339 change_id: 1,
340 };
341
342 assert_eq!(change.new_text, "Hello");
343 assert_eq!(change.old_text, "World");
344 assert_eq!(change.change_id, 1);
345 }
346
347 #[test]
348 fn test_should_reparse_threshold() {
349 let mut parser = IncrementalParser::new();
350 parser.set_reparse_threshold(100);
351
352 assert!(!parser.should_reparse());
353
354 parser.bytes_changed = 101;
355 assert!(parser.should_reparse());
356 }
357
358 #[test]
359 fn test_clear_cache() {
360 let mut parser = IncrementalParser::new();
361 parser.cached_script = Some("test".to_string());
362 parser.bytes_changed = 100;
363 parser.next_change_id = 5;
364
365 parser.clear_cache();
366
367 assert!(parser.cached_script.is_none());
368 assert_eq!(parser.bytes_changed, 0);
369 assert_eq!(parser.next_change_id, 1);
370 }
371
372 #[test]
373 fn test_error_recovery() {
374 let mut parser = IncrementalParser::new();
375
376 let content = "[Script Info]\nTitle: Test";
378 let result = parser.apply_change(
379 content,
380 Range::new(Position::new(0), Position::new(5)),
381 "New",
382 );
383 assert!(result.is_ok());
384 assert!(parser.cached_script.is_some());
385
386 parser.set_reparse_threshold(10);
388 parser.bytes_changed = 11;
389 let result = parser.apply_change(
390 content,
391 Range::new(Position::new(0), Position::new(5)),
392 "Changed",
393 );
394 assert!(result.is_ok());
395 assert_eq!(parser.bytes_changed, 0); }
397}