Skip to main content

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}