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