Skip to main content

ass_editor/core/incremental/
apply.rs

1//! Incremental change application for [`IncrementalParser`].
2//!
3//! Implements [`IncrementalParser::apply_change`], which applies a single edit
4//! via `Script::parse_partial`, tracks the resulting delta, and falls back to a
5//! full reparse when incremental parsing is unavailable or fails.
6
7use super::{DocumentChange, IncrementalParser};
8use crate::core::errors::EditorError;
9use crate::core::{Range, Result};
10use ass_core::parser::{script::ScriptDeltaOwned, Script};
11
12#[cfg(feature = "std")]
13use std::borrow::Cow;
14
15#[cfg(not(feature = "std"))]
16use alloc::{borrow::Cow, string::ToString};
17
18impl IncrementalParser {
19    /// Apply a change incrementally, returning the delta
20    pub fn apply_change(
21        &mut self,
22        document_text: &str,
23        range: Range,
24        new_text: &str,
25    ) -> Result<ScriptDeltaOwned> {
26        // If we don't have a cached script or too many changes accumulated, do full parse
27        if self.cached_script.is_none() || self.bytes_changed >= self.reparse_threshold {
28            return self.full_reparse(document_text);
29        }
30
31        // Validate range
32        if range.end.offset > document_text.len() || range.start.offset > range.end.offset {
33            return Err(EditorError::InvalidRange {
34                start: range.start.offset,
35                end: range.end.offset,
36                length: document_text.len(),
37            });
38        }
39
40        // Check if we're already on valid UTF-8 boundaries
41        let start_is_valid = range.start.offset == 0
42            || range.start.offset == document_text.len()
43            || document_text.is_char_boundary(range.start.offset);
44        let end_is_valid = range.end.offset == 0
45            || range.end.offset == document_text.len()
46            || document_text.is_char_boundary(range.end.offset);
47
48        if !start_is_valid || !end_is_valid {
49            // The range is not on valid UTF-8 boundaries - this is an error
50            // We should not silently adjust the range as it will cause undo/redo issues
51            return Err(EditorError::command_failed(
52                "Edit range is not on valid UTF-8 character boundaries",
53            ));
54        }
55
56        // Get the old text being replaced
57        let old_text = &document_text[range.start.offset..range.end.offset];
58        let (start_byte, end_byte) = (range.start.offset, range.end.offset);
59
60        // Track the change (convert to owned for storage)
61        let change = DocumentChange {
62            range,
63            new_text: Cow::Owned(new_text.to_string()),
64            old_text: Cow::Owned(old_text.to_string()),
65            #[cfg(feature = "std")]
66            timestamp: std::time::Instant::now(),
67            change_id: self.next_change_id,
68        };
69        self.next_change_id += 1;
70
71        // Update bytes changed counter
72        let change_size = new_text.len().abs_diff(old_text.len());
73        self.bytes_changed += change_size;
74
75        // Store the change for potential rollback
76        self.pending_changes.push(change);
77
78        // Convert editor Range to std::ops::Range for parse_partial
79        // Use the corrected boundaries from old_text extraction
80        let byte_range = start_byte..end_byte;
81
82        // Parse the cached script first to get a Script instance
83        let cached = self.cached_script.as_ref().ok_or_else(|| {
84            EditorError::command_failed("Cached script unavailable for incremental parsing")
85        })?;
86        let script = Script::parse(cached).map_err(EditorError::from)?;
87
88        // Apply incremental parsing
89        match script.parse_partial(byte_range, new_text) {
90            Ok(delta) => {
91                // Update cached script with the change
92                self.update_cached_script(range, new_text)?;
93                Ok(delta)
94            }
95            Err(_e) => {
96                // Fall back to full reparse on error
97                self.pending_changes.pop(); // Remove failed change
98                self.bytes_changed -= change_size;
99
100                // Log the error for debugging
101                #[cfg(feature = "std")]
102                eprintln!("Incremental parse failed, falling back to full parse: {_e}");
103
104                self.full_reparse(document_text)
105            }
106        }
107    }
108}