Skip to main content

ass_core/parser/script/
partial.rs

1//! Range-based partial reparse and span adjustment helpers.
2//!
3//! Hosts the streaming [`Script::parse_partial`] entry point used by editors for
4//! sub-2ms edits, together with the `adjust_section_spans` helper that shifts
5//! unchanged section spans after a text change.
6
7use alloc::vec::Vec;
8#[cfg(feature = "stream")]
9use alloc::{format, string::ToString};
10#[cfg(feature = "stream")]
11use core::ops::Range;
12
13use crate::parser::ast::Section;
14#[cfg(feature = "stream")]
15use crate::parser::streaming;
16#[cfg(feature = "stream")]
17use crate::Result;
18
19#[cfg(feature = "stream")]
20use super::delta::{calculate_delta, ScriptDeltaOwned};
21use super::Script;
22
23impl<'a> Script<'a> {
24    /// Parse incrementally with range-based updates for editors
25    ///
26    /// Updates only the specified range, keeping other sections unchanged.
27    /// Enables <2ms edit responsiveness for interactive editing.
28    ///
29    /// # Arguments
30    ///
31    /// * `range` - Byte range in source to re-parse
32    /// * `new_text` - Replacement text for the range
33    ///
34    /// # Returns
35    ///
36    /// Delta containing changes that can be applied to existing script.
37    ///
38    /// # Errors
39    ///
40    /// Returns an error if the new text contains malformed section headers or
41    /// other unrecoverable syntax errors in the specified range.
42    #[cfg(feature = "stream")]
43    pub fn parse_partial(&self, range: Range<usize>, new_text: &str) -> Result<ScriptDeltaOwned> {
44        // Build the modified source
45        let modified_source =
46            streaming::build_modified_source(self.source, range.clone(), new_text);
47
48        // Create a TextChange for incremental parsing
49        let change = crate::parser::incremental::TextChange {
50            range: range.clone(),
51            new_text: new_text.to_string(),
52            line_range: crate::parser::incremental::calculate_line_range(self.source, range),
53        };
54
55        // Parse incrementally
56        let new_script = self.parse_incremental(&modified_source, &change)?;
57
58        // Calculate delta
59        let delta = calculate_delta(self, &new_script);
60
61        // Convert to owned format
62        let mut owned_delta = ScriptDeltaOwned {
63            added: Vec::new(),
64            modified: Vec::new(),
65            removed: Vec::new(),
66            new_issues: Vec::new(),
67        };
68
69        // Convert added sections
70        for section in delta.added {
71            owned_delta.added.push(format!("{section:?}"));
72        }
73
74        // Convert modified sections
75        for (idx, section) in delta.modified {
76            owned_delta.modified.push((idx, format!("{section:?}")));
77        }
78
79        // Convert removed sections
80        owned_delta.removed = delta.removed;
81
82        // Convert new issues
83        owned_delta.new_issues = delta.new_issues;
84
85        Ok(owned_delta)
86    }
87
88    /// Adjust section spans for unchanged sections after a text change
89    pub(super) fn adjust_section_spans(
90        section: &Section<'a>,
91        change: &crate::parser::incremental::TextChange,
92    ) -> Section<'a> {
93        use crate::parser::ast::Span;
94
95        // Calculate the offset caused by the change
96        let new_len = change.new_text.len();
97        let old_len = change.range.end - change.range.start;
98
99        // Helper to adjust a span using safe arithmetic
100        let adjust_span = |span: &Span| -> Span {
101            let new_start = if new_len >= old_len {
102                span.start + (new_len - old_len)
103            } else {
104                span.start.saturating_sub(old_len - new_len)
105            };
106
107            let new_end = if new_len >= old_len {
108                span.end + (new_len - old_len)
109            } else {
110                span.end.saturating_sub(old_len - new_len)
111            };
112
113            Span::new(new_start, new_end, span.line, span.column)
114        };
115
116        // Adjust all spans in the section
117        match section {
118            Section::ScriptInfo(info) => {
119                let mut new_info = info.clone();
120                new_info.span = adjust_span(&info.span);
121                Section::ScriptInfo(new_info)
122            }
123            Section::Styles(styles) => {
124                let new_styles: Vec<_> = styles
125                    .iter()
126                    .map(|style| {
127                        let mut new_style = style.clone();
128                        new_style.span = adjust_span(&style.span);
129                        new_style
130                    })
131                    .collect();
132                Section::Styles(new_styles)
133            }
134            Section::Events(events) => {
135                let new_events: Vec<_> = events
136                    .iter()
137                    .map(|event| {
138                        let mut new_event = event.clone();
139                        new_event.span = adjust_span(&event.span);
140                        new_event
141                    })
142                    .collect();
143                Section::Events(new_events)
144            }
145            Section::Fonts(fonts) => {
146                let new_fonts: Vec<_> = fonts
147                    .iter()
148                    .map(|font| {
149                        let mut new_font = font.clone();
150                        new_font.span = adjust_span(&font.span);
151                        new_font
152                    })
153                    .collect();
154                Section::Fonts(new_fonts)
155            }
156            Section::Graphics(graphics) => {
157                let new_graphics: Vec<_> = graphics
158                    .iter()
159                    .map(|graphic| {
160                        let mut new_graphic = graphic.clone();
161                        new_graphic.span = adjust_span(&graphic.span);
162                        new_graphic
163                    })
164                    .collect();
165                Section::Graphics(new_graphics)
166            }
167        }
168    }
169}