ass_core/parser/script/incremental.rs
1//! Incremental reparse support for editor-driven text changes.
2//!
3//! Implements [`Script::affected_sections`], which maps a text change to the
4//! sections it touches, and [`Script::parse_incremental`], which reparses only
5//! those sections while shifting the spans of unchanged sections.
6
7use alloc::{vec, vec::Vec};
8
9use crate::parser::ast::{Section, SectionType};
10use crate::parser::main::Parser;
11use crate::Result;
12
13use super::Script;
14
15impl<'a> Script<'a> {
16 // Incremental parsing support
17
18 /// Determine which sections are affected by a text change
19 ///
20 /// # Arguments
21 ///
22 /// * `change` - The text change to analyze
23 ///
24 /// # Returns
25 ///
26 /// A vector of section types that are affected by the change
27 #[must_use]
28 pub fn affected_sections(
29 &self,
30 change: &crate::parser::incremental::TextChange,
31 ) -> Vec<SectionType> {
32 self.sections
33 .iter()
34 .filter(|section| {
35 section.span().is_some_and(|span| {
36 let section_range = span.start..span.end;
37
38 // Check if change overlaps with section
39 let overlaps = change.range.start < section_range.end
40 && change.range.end > section_range.start;
41
42 // Also check if this is an insertion at the end of the section
43 // This handles cases like adding a new event at the end of the Events section
44 let inserts_at_end =
45 change.range.is_empty() && change.range.start == section_range.end;
46
47 overlaps || inserts_at_end
48 })
49 })
50 .map(Section::section_type)
51 .collect()
52 }
53
54 /// Parse only changed portions and create new Script
55 ///
56 /// This method performs incremental parsing by identifying affected sections
57 /// and reparsing only those sections while preserving others.
58 ///
59 /// # Arguments
60 ///
61 /// * `new_source` - The complete new source text after the change
62 /// * `change` - Description of what changed in the text
63 ///
64 /// # Returns
65 ///
66 /// A new Script with the changes applied
67 ///
68 /// # Errors
69 ///
70 /// Returns parse errors if affected sections cannot be reparsed
71 pub fn parse_incremental(
72 &self,
73 new_source: &'a str,
74 change: &crate::parser::incremental::TextChange,
75 ) -> Result<Self> {
76 use crate::parser::sections::SectionFormats;
77
78 // Step 1: Identify affected sections
79 let affected_sections = self.affected_sections(change);
80
81 if affected_sections.is_empty() {
82 // Change was in whitespace/comments only
83 return Ok(Script::from_parts(
84 new_source,
85 self.version(),
86 self.sections.clone(),
87 vec![], // Clear issues, will be recalculated
88 self.styles_format.clone(),
89 self.events_format.clone(),
90 ));
91 }
92
93 // Step 2: Build section formats from existing script
94 let formats = SectionFormats {
95 styles_format: self.styles_format().map(<[&str]>::to_vec),
96 events_format: self.events_format().map(<[&str]>::to_vec),
97 };
98
99 // Step 3: Prepare new sections
100 let mut new_sections = Vec::with_capacity(self.sections.len());
101
102 // We need to find where each section actually starts in the document
103 // including its header. The current spans only track content.
104 let section_headers = [
105 ("[Script Info]", SectionType::ScriptInfo),
106 ("[V4+ Styles]", SectionType::Styles),
107 ("[Events]", SectionType::Events),
108 ("[Fonts]", SectionType::Fonts),
109 ("[Graphics]", SectionType::Graphics),
110 ];
111
112 // Step 4: Process each section
113 for (idx, section) in self.sections.iter().enumerate() {
114 let section_type = section.section_type();
115
116 if affected_sections.contains(§ion_type) {
117 // Find the section header in the new source
118 let header_str = section_headers
119 .iter()
120 .find(|(_, t)| *t == section_type)
121 .map_or("[Unknown]", |(h, _)| *h);
122
123 // Find where this section starts in the new source
124 if let Some(header_pos) = new_source.find(header_str) {
125 // Find the end of this section (start of next section or end of file)
126 let section_end = if idx + 1 < self.sections.len() {
127 // Find the next section's header
128 let next_section_type = self.sections[idx + 1].section_type();
129 let next_header = section_headers
130 .iter()
131 .find(|(_, t)| *t == next_section_type)
132 .map_or("[Unknown]", |(h, _)| *h);
133
134 new_source[header_pos + header_str.len()..]
135 .find(next_header)
136 .map_or(new_source.len(), |pos| header_pos + header_str.len() + pos)
137 } else {
138 new_source.len()
139 };
140
141 // Extract the full section text including header
142 let section_text = &new_source[header_pos..section_end];
143
144 // Parse this section using a fresh parser
145 let parser = Parser::new(section_text);
146 let parsed_script = parser.parse();
147
148 // The parser returns a Script, extract sections from it
149 // We only want the one matching our type
150 if let Some(parsed_section) = parsed_script
151 .sections
152 .into_iter()
153 .find(|s| s.section_type() == section_type)
154 {
155 new_sections.push(parsed_section);
156 }
157 }
158 } else {
159 // Section unchanged, but might need span adjustment if change was before it
160 let section_span = section.span();
161 if let Some(span) = section_span {
162 if change.range.end <= span.start {
163 // Change was before this section, adjust its spans
164 new_sections.push(Self::adjust_section_spans(section, change));
165 } else {
166 // Change was after this section, keep as-is
167 new_sections.push(section.clone());
168 }
169 } else {
170 new_sections.push(section.clone());
171 }
172 }
173 }
174
175 // Step 5: Create new Script with updated sections
176 Ok(Script::from_parts(
177 new_source,
178 self.version(),
179 new_sections,
180 vec![], // Issues will be recalculated
181 formats.styles_format.clone(),
182 formats.events_format.clone(),
183 ))
184 }
185}