Skip to main content

ass_editor/commands/style_commands/
apply.rs

1//! Style application command for ASS documents.
2//!
3//! Provides [`ApplyStyleCommand`] to retarget events from one style to
4//! another, with an optional text filter on event lines.
5
6use crate::commands::{CommandResult, EditorCommand};
7use crate::core::{EditorDocument, EditorError, Position, Range, Result};
8
9#[cfg(not(feature = "std"))]
10use alloc::{
11    format,
12    string::{String, ToString},
13    vec::Vec,
14};
15
16/// Command to apply a style to events (change all events using one style to another)
17#[derive(Debug, Clone)]
18pub struct ApplyStyleCommand {
19    pub old_style: String,
20    pub new_style: String,
21    pub event_filter: Option<String>, // Optional filter for event text
22    pub description: Option<String>,
23}
24
25impl ApplyStyleCommand {
26    /// Create a new style application command
27    pub fn new(old_style: String, new_style: String) -> Self {
28        Self {
29            old_style,
30            new_style,
31            event_filter: None,
32            description: None,
33        }
34    }
35
36    /// Only apply to events containing specific text
37    pub fn with_filter(mut self, filter: String) -> Self {
38        self.event_filter = Some(filter);
39        self
40    }
41
42    /// Set custom description
43    #[must_use]
44    pub fn with_description(mut self, description: String) -> Self {
45        self.description = Some(description);
46        self
47    }
48}
49
50impl EditorCommand for ApplyStyleCommand {
51    fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
52        let mut content = document.text();
53        let mut changes_made = 0;
54        let mut total_range: Option<Range> = None;
55
56        // Find events section
57        let events_start = content
58            .find("[Events]")
59            .ok_or_else(|| EditorError::command_failed("Events section not found"))?;
60
61        // Skip format line
62        let events_content_start = content[events_start..]
63            .find('\n')
64            .map(|pos| events_start + pos + 1)
65            .ok_or_else(|| EditorError::command_failed("Invalid events section format"))?;
66
67        // Skip format line again (Format: ...)
68        let first_event_start = content[events_content_start..]
69            .find('\n')
70            .map(|pos| events_content_start + pos + 1)
71            .unwrap_or(events_content_start);
72
73        let mut search_pos = first_event_start;
74
75        while search_pos < content.len() {
76            // Find next event line
77            let line_start = search_pos;
78            let line_end = content[line_start..]
79                .find('\n')
80                .map(|pos| line_start + pos)
81                .unwrap_or(content.len());
82
83            if line_start >= line_end {
84                break;
85            }
86
87            let line = &content[line_start..line_end];
88
89            // Check if this is an event line
90            if line.starts_with("Dialogue:") || line.starts_with("Comment:") {
91                let parts: Vec<&str> = line.split(',').collect();
92
93                // Check if this event uses the old style (typically 4th field after event type)
94                if parts.len() > 4 && parts[3].trim() == self.old_style {
95                    // Apply filter if specified
96                    let should_update = if let Some(ref filter) = self.event_filter {
97                        line.contains(filter)
98                    } else {
99                        true
100                    };
101
102                    if should_update {
103                        // Replace the old style with new style
104                        let updated_line = line.replace(
105                            &format!(",{},", self.old_style),
106                            &format!(",{},", self.new_style),
107                        );
108
109                        // Update the document
110                        let range = Range::new(Position::new(line_start), Position::new(line_end));
111                        document.replace(range, &updated_line)?;
112
113                        // Update content for next iteration (this is inefficient but correct)
114                        content = document.text();
115
116                        // Track overall range
117                        let change_range = Range::new(
118                            Position::new(line_start),
119                            Position::new(line_start + updated_line.len()),
120                        );
121                        total_range = Some(match total_range {
122                            Some(existing) => existing.union(&change_range),
123                            None => change_range,
124                        });
125
126                        changes_made += 1;
127                    }
128                }
129            } else if line.starts_with('[') && line != "[Events]" {
130                // Stop at next section
131                break;
132            }
133
134            search_pos = line_end + 1;
135        }
136
137        if changes_made > 0 {
138            Ok(CommandResult::success_with_change(
139                total_range.unwrap_or(Range::new(Position::new(0), Position::new(0))),
140                Position::new(content.len()),
141            )
142            .with_message(format!(
143                "Applied style '{}' to {} events",
144                self.new_style, changes_made
145            )))
146        } else {
147            Ok(CommandResult::success()
148                .with_message("No events found matching the criteria".to_string()))
149        }
150    }
151
152    fn description(&self) -> &str {
153        self.description
154            .as_deref()
155            .unwrap_or("Apply style to events")
156    }
157
158    fn memory_usage(&self) -> usize {
159        core::mem::size_of::<Self>()
160            + self.old_style.len()
161            + self.new_style.len()
162            + self.event_filter.as_ref().map_or(0, |f| f.len())
163            + self.description.as_ref().map_or(0, |d| d.len())
164    }
165}