ass_core/parser/script/delta.rs
1//! Script delta types and computation for streaming editor updates.
2//!
3//! Defines the borrowed [`ScriptDelta`] and owned [`ScriptDeltaOwned`] change
4//! descriptions and the [`calculate_delta`] routine that compares two scripts
5//! while ignoring span-only differences.
6
7use alloc::{string::String, vec::Vec};
8
9use crate::parser::ast::Section;
10use crate::parser::errors::ParseIssue;
11
12use super::delta_eq::sections_equal_ignoring_spans;
13use super::Script;
14
15/// Incremental parsing delta for efficient editor updates
16#[derive(Debug, Clone)]
17pub struct ScriptDelta<'a> {
18 /// Sections that were added
19 pub added: Vec<Section<'a>>,
20
21 /// Sections that were modified (old index -> new section)
22 pub modified: Vec<(usize, Section<'a>)>,
23
24 /// Section indices that were removed
25 pub removed: Vec<usize>,
26
27 /// New parse issues
28 pub new_issues: Vec<ParseIssue>,
29}
30
31/// Calculate differences between two Scripts
32///
33/// Analyzes the differences between old and new scripts and returns
34/// a delta containing the minimal set of changes needed to transform
35/// the old script into the new one.
36///
37/// # Arguments
38///
39/// * `old_script` - The original script
40/// * `new_script` - The updated script
41///
42/// # Returns
43///
44/// A `ScriptDelta` describing the changes
45#[must_use]
46pub fn calculate_delta<'a>(old_script: &Script<'a>, new_script: &Script<'a>) -> ScriptDelta<'a> {
47 let mut added = Vec::new();
48 let mut modified = Vec::new();
49 let mut removed = Vec::new();
50
51 // Create maps for efficient lookup
52 let old_sections: Vec<_> = old_script.sections().iter().collect();
53 let new_sections: Vec<_> = new_script.sections().iter().collect();
54
55 // Find modifications and removals
56 for (idx, old_section) in old_sections.iter().enumerate() {
57 let old_type = old_section.section_type();
58
59 // Look for matching section in new script
60 if let Some((_new_idx, new_section)) = new_sections
61 .iter()
62 .enumerate()
63 .find(|(_, s)| s.section_type() == old_type)
64 {
65 // Check if content changed (ignoring spans)
66 if !sections_equal_ignoring_spans(old_section, new_section) {
67 modified.push((idx, (*new_section).clone()));
68 }
69 } else {
70 // Section was removed
71 removed.push(idx);
72 }
73 }
74
75 // Find additions
76 for new_section in &new_sections {
77 let new_type = new_section.section_type();
78
79 // Check if this type exists in old script
80 if !old_sections.iter().any(|s| s.section_type() == new_type) {
81 added.push((*new_section).clone());
82 }
83 }
84
85 // Calculate new issues
86 // For simplicity, just take all issues from the new script
87 // In a more sophisticated implementation, we could diff the issues
88 let new_issues: Vec<_> = new_script.issues().to_vec();
89
90 ScriptDelta {
91 added,
92 modified,
93 removed,
94 new_issues,
95 }
96}
97
98/// Owned variant of `ScriptDelta` for incremental parsing with lifetime independence
99#[derive(Debug, Clone)]
100pub struct ScriptDeltaOwned {
101 /// Sections that were added (serialized as source text)
102 pub added: Vec<String>,
103
104 /// Sections that were modified (old index -> new section as source text)
105 pub modified: Vec<(usize, String)>,
106
107 /// Section indices that were removed
108 pub removed: Vec<usize>,
109
110 /// New parse issues
111 pub new_issues: Vec<ParseIssue>,
112}
113
114impl ScriptDelta<'_> {
115 /// Check if the delta contains no changes
116 #[must_use]
117 pub fn is_empty(&self) -> bool {
118 self.added.is_empty()
119 && self.modified.is_empty()
120 && self.removed.is_empty()
121 && self.new_issues.is_empty()
122 }
123}