Skip to main content

ass_editor/core/document/
delta_apply.rs

1//! Applying script deltas to the document text
2//!
3//! Records and applies `ScriptDeltaOwned` changes by translating section
4//! additions, modifications, and removals into raw text edits, then
5//! revalidating the result.
6
7use super::EditorDocument;
8use crate::commands::CommandResult;
9use crate::core::errors::{EditorError, Result};
10use crate::core::position::{Position, Range};
11use ass_core::parser::ast::Section;
12use ass_core::parser::script::ScriptDeltaOwned;
13use ass_core::parser::Script;
14
15#[cfg(not(feature = "std"))]
16use alloc::string::ToString;
17
18impl EditorDocument {
19    /// Apply a script delta and record it with undo data
20    pub fn apply_script_delta(&mut self, delta: ScriptDeltaOwned) -> Result<()> {
21        use crate::core::history::Operation;
22
23        // Capture undo data before applying
24        let undo_data = self.capture_delta_undo_data(&delta)?;
25
26        // Apply delta
27        self.apply_script_delta_internal(delta.clone())?;
28
29        // Record with undo data
30        let operation = Operation::Delta {
31            forward: delta,
32            undo_data,
33        };
34
35        let result = CommandResult::success();
36        self.history
37            .record_operation(operation, "Apply delta".to_string(), &result);
38
39        Ok(())
40    }
41
42    /// Apply a script delta for efficient incremental parsing (internal)
43    fn apply_script_delta_internal(&mut self, delta: ScriptDeltaOwned) -> Result<()> {
44        // Parse the current script to get sections
45        let current_content = self.text();
46        let script = Script::parse(&current_content).map_err(EditorError::from)?;
47
48        // Apply removals first (in reverse order to maintain indices)
49        let mut removed_indices = delta.removed.clone();
50        removed_indices.sort_by(|a, b| b.cmp(a)); // Sort descending
51
52        for index in removed_indices {
53            if index < script.sections().len() {
54                // Find the section's text range and remove it
55                let section = &script.sections()[index];
56                let start_offset = self.find_section_start(section)?;
57                let end_offset = self.find_section_end(section)?;
58
59                self.delete_raw(Range::new(
60                    Position::new(start_offset),
61                    Position::new(end_offset),
62                ))?;
63            }
64        }
65
66        // Apply modifications
67        for (index, new_section_text) in delta.modified {
68            if index < script.sections().len() {
69                // Find the section's text range
70                let section = &script.sections()[index];
71                let start_offset = self.find_section_start(section)?;
72                let end_offset = self.find_section_end(section)?;
73
74                // Replace with new section text
75                self.replace_raw(
76                    Range::new(Position::new(start_offset), Position::new(end_offset)),
77                    &new_section_text,
78                )?;
79            }
80        }
81
82        // Apply additions
83        for section_text in delta.added {
84            // Add new sections at the end of the document
85            let end_pos = Position::new(self.len_bytes());
86
87            // Ensure proper newline before new section
88            if self.len_bytes() > 0 && !self.text().ends_with('\n') {
89                self.insert_raw(end_pos, "\n")?;
90            }
91
92            self.insert_raw(Position::new(self.len_bytes()), &section_text)?;
93
94            // Ensure trailing newline
95            if !section_text.ends_with('\n') {
96                self.insert_raw(Position::new(self.len_bytes()), "\n")?;
97            }
98        }
99
100        // Validate the result
101        let _ = Script::parse(&self.text()).map_err(EditorError::from)?;
102
103        Ok(())
104    }
105
106    /// Find the start offset of a section in the document
107    fn find_section_start(&self, section: &Section) -> Result<usize> {
108        // Get the section header text
109        let header = match section {
110            Section::ScriptInfo(_) => "[Script Info]",
111            Section::Styles(_) => "[V4+ Styles]",
112            Section::Events(_) => "[Events]",
113            Section::Fonts(_) => "[Fonts]",
114            Section::Graphics(_) => "[Graphics]",
115        };
116
117        // Find the header in the document
118        if let Some(pos) = self.text().find(header) {
119            Ok(pos)
120        } else {
121            Err(EditorError::SectionNotFound {
122                section: header.to_string(),
123            })
124        }
125    }
126
127    /// Find the end offset of a section in the document
128    fn find_section_end(&self, section: &Section) -> Result<usize> {
129        let start = self.find_section_start(section)?;
130        let content = &self.text()[start..];
131
132        // Find the next section header or end of document
133        let section_headers = [
134            "[Script Info]",
135            "[V4+ Styles]",
136            "[Events]",
137            "[Fonts]",
138            "[Graphics]",
139        ];
140
141        let mut end_offset = content.len();
142        for header in &section_headers {
143            if let Some(pos) = content.find(header) {
144                if pos > 0 {
145                    end_offset = end_offset.min(pos);
146                }
147            }
148        }
149
150        Ok(start + end_offset)
151    }
152}